diff --git a/.github/workflows/sync-release-to-main.yaml b/.github/workflows/sync-release-to-main.yaml new file mode 100644 index 0000000..a641658 --- /dev/null +++ b/.github/workflows/sync-release-to-main.yaml @@ -0,0 +1,54 @@ +name: Sync release to main + +on: + workflow_run: + workflows: [Release] + types: [completed] + branches: [release] + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Check if release is ahead of main + id: check + run: | + git fetch origin main release + AHEAD=$(git rev-list --count origin/main..origin/release) + echo "ahead=$AHEAD" >> $GITHUB_OUTPUT + + - name: Create sync PR + if: steps.check.outputs.ahead != '0' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + EXISTING=$(gh pr list --repo ${{ github.repository }} --base main --head release --state open --json number --jq '.[0].number') + if [ -n "$EXISTING" ]; then + echo "Sync PR #$EXISTING already exists" + else + gh pr create \ + --repo ${{ github.repository }} \ + --base main \ + --head release \ + --title "chore: sync release to main" \ + --body "Automated sync of release tags back to main." + fi + + - name: Auto-merge sync PR + if: steps.check.outputs.ahead != '0' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR=$(gh pr list --repo ${{ github.repository }} --base main --head release --state open --json number --jq '.[0].number') + if [ -n "$PR" ]; then + gh pr merge "$PR" --repo ${{ github.repository }} --merge + fi diff --git a/package-lock.json b/package-lock.json index 4f1cb28..fc5f849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@aws-sdk/credential-providers": "^3.1024.0", "@smithy/shared-ini-file-loader": "^4.4.7", "@tigrisdata/iam": "^1.4.1", - "@tigrisdata/storage": "^2.16.2", + "@tigrisdata/storage": "^3.0.0", "commander": "^14.0.3", "enquirer": "^2.4.1", "jose": "^6.2.2", @@ -4015,9 +4015,9 @@ } }, "node_modules/@tigrisdata/storage": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@tigrisdata/storage/-/storage-2.16.2.tgz", - "integrity": "sha512-O17sUXp+8o5d+fjUwDYO4RhdRloJ3KazM+ICG7pbo682aKX5ROpnYH8zJn3qpzLn49CJy9TpdyknjuxrTX/aLA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tigrisdata/storage/-/storage-3.0.0.tgz", + "integrity": "sha512-Rhw+aEOpl2bcgDhIymAguX2m178TYdco+lmX+zxYHw+P9jX8v4euwnZwRSb/+YwqmEawhBeapdNkCgIsBIVZ8g==", "license": "MIT", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", diff --git a/package.json b/package.json index d9b0bcc..4532e17 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@aws-sdk/credential-providers": "^3.1024.0", "@smithy/shared-ini-file-loader": "^4.4.7", "@tigrisdata/iam": "^1.4.1", - "@tigrisdata/storage": "^2.16.2", + "@tigrisdata/storage": "^3.0.0", "commander": "^14.0.3", "enquirer": "^2.4.1", "jose": "^6.2.2", diff --git a/src/lib/access-keys/list.ts b/src/lib/access-keys/list.ts index 261fa25..6e6b71d 100644 --- a/src/lib/access-keys/list.ts +++ b/src/lib/access-keys/list.ts @@ -1,7 +1,7 @@ import { getIAMConfig } from '@auth/iam.js'; import { listAccessKeys } from '@tigrisdata/iam'; import { failWithError } from '@utils/exit.js'; -import { formatOutput, formatPaginatedOutput } from '@utils/format.js'; +import { formatPaginatedOutput } from '@utils/format.js'; import { msg, printEmpty, @@ -17,7 +17,7 @@ export default async function list(options: Record) { printStart(context); const format = getFormat(options); - const { limit, pageToken, isPaginated } = getPaginationOptions(options); + const { limit, pageToken } = getPaginationOptions(options); const config = await getIAMConfig(context); @@ -52,15 +52,13 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated - ? formatPaginatedOutput(keys, format!, 'keys', 'key', columns, { - paginationToken: nextToken, - }) - : formatOutput(keys, format!, 'keys', 'key', columns); + const output = formatPaginatedOutput(keys, format!, 'keys', 'key', columns, { + paginationToken: nextToken, + }); console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } diff --git a/src/lib/buckets/list.ts b/src/lib/buckets/list.ts index f59b0b8..243196f 100644 --- a/src/lib/buckets/list.ts +++ b/src/lib/buckets/list.ts @@ -28,7 +28,7 @@ export default async function list(options: Record) { } const { data, error } = await listBuckets({ - ...(forksOf || !isPaginated + ...(forksOf ? {} : { ...(limit !== undefined ? { limit } : {}), @@ -57,7 +57,7 @@ export default async function list(options: Record) { failWithError(context, infoError); } - if (!bucketInfo.hasForks) { + if (!bucketInfo.forkInfo?.hasChildren) { printEmpty(context); return; } @@ -67,7 +67,10 @@ export default async function list(options: Record) { for (const bucket of data.buckets) { if (bucket.name === forksOf) continue; const { data: info } = await getBucketInfo(bucket.name, { config }); - if (info?.sourceBucketName === forksOf) { + const isChildOf = info?.forkInfo?.parents?.some( + (p) => p.bucketName === forksOf + ); + if (isChildOf) { forks.push({ name: bucket.name, created: bucket.creationDate }); } } @@ -99,15 +102,18 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated - ? formatPaginatedOutput(buckets, format!, 'buckets', 'bucket', columns, { - paginationToken: nextToken, - }) - : formatOutput(buckets, format!, 'buckets', 'bucket', columns); + const output = formatPaginatedOutput( + buckets, + format!, + 'buckets', + 'bucket', + columns, + { paginationToken: nextToken } + ); console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } diff --git a/src/lib/forks/list.ts b/src/lib/forks/list.ts index 5dee405..62a51cc 100644 --- a/src/lib/forks/list.ts +++ b/src/lib/forks/list.ts @@ -28,7 +28,7 @@ export default async function list(options: Record) { failWithError(context, infoError); } - if (!bucketInfo.hasForks) { + if (!bucketInfo.forkInfo?.hasChildren) { printEmpty(context); return; } @@ -47,7 +47,10 @@ export default async function list(options: Record) { if (bucket.name === name) continue; const { data: info } = await getBucketInfo(bucket.name, { config }); - if (info?.sourceBucketName === name) { + const isChildOf = info?.forkInfo?.parents?.some( + (p) => p.bucketName === name + ); + if (isChildOf) { forks.push({ name: bucket.name, created: bucket.creationDate, diff --git a/src/lib/iam/policies/list.ts b/src/lib/iam/policies/list.ts index 5004e0e..15ba76b 100644 --- a/src/lib/iam/policies/list.ts +++ b/src/lib/iam/policies/list.ts @@ -1,7 +1,7 @@ import { getOAuthIAMConfig } from '@auth/iam.js'; import { listPolicies } from '@tigrisdata/iam'; import { failWithError } from '@utils/exit.js'; -import { formatOutput, formatPaginatedOutput } from '@utils/format.js'; +import { formatPaginatedOutput } from '@utils/format.js'; import { msg, printEmpty, @@ -17,7 +17,7 @@ export default async function list(options: Record) { printStart(context); const format = getFormat(options); - const { limit, pageToken, isPaginated } = getPaginationOptions(options); + const { limit, pageToken } = getPaginationOptions(options); const iamConfig = await getOAuthIAMConfig(context); @@ -62,15 +62,18 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated - ? formatPaginatedOutput(policies, format!, 'policies', 'policy', columns, { - paginationToken: nextToken, - }) - : formatOutput(policies, format!, 'policies', 'policy', columns); + const output = formatPaginatedOutput( + policies, + format!, + 'policies', + 'policy', + columns, + { paginationToken: nextToken } + ); console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } diff --git a/src/lib/ls.ts b/src/lib/ls.ts index 5451d8d..2bfb789 100644 --- a/src/lib/ls.ts +++ b/src/lib/ls.ts @@ -1,8 +1,9 @@ import { getStorageConfig } from '@auth/provider.js'; import { list, listBuckets } from '@tigrisdata/storage'; import { exitWithError } from '@utils/exit.js'; -import { formatOutput, formatSize } from '@utils/format.js'; -import { getFormat, getOption } from '@utils/options.js'; +import { formatPaginatedOutput, formatSize } from '@utils/format.js'; +import { printPaginationHint } from '@utils/messages.js'; +import { getFormat, getOption, getPaginationOptions } from '@utils/options.js'; import { parseAnyPath } from '@utils/path.js'; export default async function ls(options: Record) { @@ -13,11 +14,16 @@ export default async function ls(options: Record) { 'snapshot', ]); const format = getFormat(options); + const { limit, pageToken } = getPaginationOptions(options); if (!pathString) { // No path provided, list all buckets const config = await getStorageConfig(); - const { data, error } = await listBuckets({ config }); + const { data, error } = await listBuckets({ + ...(limit !== undefined ? { limit } : {}), + ...(pageToken ? { paginationToken: pageToken } : {}), + config, + }); if (error) { exitWithError(error); @@ -28,12 +34,28 @@ export default async function ls(options: Record) { created: bucket.creationDate, })); - const output = formatOutput(buckets, format!, 'buckets', 'bucket', [ + const columns = [ { key: 'name', header: 'Name' }, { key: 'created', header: 'Created' }, - ]); + ]; + + const nextToken = data.paginationToken || undefined; + + const output = formatPaginatedOutput( + buckets, + format!, + 'buckets', + 'bucket', + columns, + { paginationToken: nextToken } + ); console.log(output); + + if (format !== 'json' && format !== 'xml') { + printPaginationHint(nextToken); + } + return; } @@ -51,6 +73,8 @@ export default async function ls(options: Record) { const { data, error } = await list({ prefix, ...(snapshotVersion ? { snapshotVersion } : {}), + ...(limit !== undefined ? { limit } : {}), + ...(pageToken ? { paginationToken: pageToken } : {}), config: { ...config, bucket, @@ -84,11 +108,26 @@ export default async function ls(options: Record) { item.key !== '' && arr.findIndex((i) => i.key === item.key) === index ); - const output = formatOutput(objects, format!, 'objects', 'object', [ + const columns = [ { key: 'key', header: 'Key' }, { key: 'size', header: 'Size' }, { key: 'modified', header: 'Modified' }, - ]); + ]; + + const nextToken = data.paginationToken || undefined; + + const output = formatPaginatedOutput( + objects, + format!, + 'objects', + 'object', + columns, + { paginationToken: nextToken } + ); console.log(output); + + if (format !== 'json' && format !== 'xml') { + printPaginationHint(nextToken); + } } diff --git a/src/lib/objects/list.ts b/src/lib/objects/list.ts index 5234c0c..f9ff4b0 100644 --- a/src/lib/objects/list.ts +++ b/src/lib/objects/list.ts @@ -1,11 +1,7 @@ import { getStorageConfig } from '@auth/provider.js'; import { list } from '@tigrisdata/storage'; import { failWithError } from '@utils/exit.js'; -import { - formatOutput, - formatPaginatedOutput, - formatSize, -} from '@utils/format.js'; +import { formatPaginatedOutput, formatSize } from '@utils/format.js'; import { msg, printEmpty, @@ -29,7 +25,7 @@ export default async function listObjects(options: Record) { 'snapshotVersion', 'snapshot', ]); - const { limit, pageToken, isPaginated } = getPaginationOptions(options); + const { limit, pageToken } = getPaginationOptions(options); if (!bucketArg) { failWithError(context, 'Bucket name is required'); @@ -75,15 +71,18 @@ export default async function listObjects(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated - ? formatPaginatedOutput(objects, format!, 'objects', 'object', columns, { - paginationToken: nextToken, - }) - : formatOutput(objects, format!, 'objects', 'object', columns); + const output = formatPaginatedOutput( + objects, + format!, + 'objects', + 'object', + columns, + { paginationToken: nextToken } + ); console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } diff --git a/src/lib/snapshots/list.ts b/src/lib/snapshots/list.ts index 4875ef6..3cfbf9c 100644 --- a/src/lib/snapshots/list.ts +++ b/src/lib/snapshots/list.ts @@ -1,9 +1,15 @@ import { getStorageConfig } from '@auth/provider.js'; import { listBucketSnapshots } from '@tigrisdata/storage'; import { failWithError } from '@utils/exit.js'; -import { formatOutput } from '@utils/format.js'; -import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; -import { getFormat, getOption } from '@utils/options.js'; +import { formatPaginatedOutput } from '@utils/format.js'; +import { + msg, + printEmpty, + printPaginationHint, + printStart, + printSuccess, +} from '@utils/messages.js'; +import { getFormat, getOption, getPaginationOptions } from '@utils/options.js'; const context = msg('snapshots', 'list'); @@ -12,6 +18,7 @@ export default async function list(options: Record) { const name = getOption(options, ['name']); const format = getFormat(options); + const { limit, pageToken } = getPaginationOptions(options); if (!name) { failWithError(context, 'Bucket name is required'); @@ -19,29 +26,49 @@ export default async function list(options: Record) { const config = await getStorageConfig(); - const { data, error } = await listBucketSnapshots(name, { config }); + const { data, error } = await listBucketSnapshots(name, { + ...(limit !== undefined ? { limit } : {}), + ...(pageToken ? { paginationToken: pageToken } : {}), + config, + }); if (error) { failWithError(context, error); } - if (!data || data.length === 0) { + if (!data.snapshots || data.snapshots.length === 0) { printEmpty(context); return; } - const snapshots = data.map((snapshot) => ({ + const snapshots = data.snapshots.map((snapshot) => ({ name: snapshot.name || '', version: snapshot.version || '', created: snapshot.creationDate, })); - const output = formatOutput(snapshots, format!, 'snapshots', 'snapshot', [ + const columns = [ { key: 'name', header: 'Name' }, { key: 'version', header: 'Version' }, { key: 'created', header: 'Created' }, - ]); + ]; + + const nextToken = data.paginationToken || undefined; + + const output = formatPaginatedOutput( + snapshots, + format!, + 'snapshots', + 'snapshot', + columns, + { paginationToken: nextToken } + ); console.log(output); + + if (format !== 'json' && format !== 'xml') { + printPaginationHint(nextToken); + } + printSuccess(context, { count: snapshots.length }); } diff --git a/src/specs.yaml b/src/specs.yaml index e32655a..0692b7d 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -292,6 +292,11 @@ commands: description: Output format options: [json, table, xml] default: table + - name: limit + description: Maximum number of items to return per page + - name: page-token + description: Pagination token from a previous request to fetch the next page + alias: pt # mk - name: mk @@ -1115,6 +1120,11 @@ commands: description: Output format options: [json, table, xml] default: table + - name: limit + description: Maximum number of items to return per page + - name: page-token + description: Pagination token from a previous request to fetch the next page + alias: pt # take - name: take description: Take a new snapshot of the bucket's current state. Optionally provide a name for the snapshot diff --git a/test/cli.test.ts b/test/cli.test.ts index cd45a27..e0b06cc 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -275,12 +275,11 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { const listResult = runCli('buckets list --format json'); if (listResult.exitCode === 0 && listResult.stdout.trim()) { try { - const buckets = JSON.parse(listResult.stdout.trim()) as Array<{ - name: string; - created: string; - }>; + const parsed = JSON.parse(listResult.stdout.trim()) as { + items: Array<{ name: string; created: string }>; + }; const now = Date.now(); - for (const bucket of buckets) { + for (const bucket of parsed.items) { if (!bucket.name.startsWith('tigris-cli-test-')) continue; const age = now - new Date(bucket.created).getTime(); if (age > staleThresholdMs) { @@ -1156,10 +1155,10 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { const result = runCli('buckets list --format json'); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout.trim()); - expect(Array.isArray(parsed)).toBe(true); - expect(parsed.some((b: { name: string }) => b.name === testBucket)).toBe( - true - ); + expect(Array.isArray(parsed.items)).toBe(true); + expect( + parsed.items.some((b: { name: string }) => b.name === testBucket) + ).toBe(true); }); }); @@ -1810,10 +1809,10 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { const result = runCli(`snapshots list ${snapBucket} --format json`); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout.trim()); - expect(Array.isArray(parsed)).toBe(true); - expect(parsed.length).toBeGreaterThan(0); + expect(Array.isArray(parsed.items)).toBe(true); + expect(parsed.items.length).toBeGreaterThan(0); // Save version for later tests - snapshotVersion = parsed[0].version; + snapshotVersion = parsed.items[0].version; expect(snapshotVersion).toBeTruthy(); });