Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- `cleanup-unfinished`: list unfinished multipart uploads under a prefix and cancel idle uploads, with `dry-run` preview support, explicit whole-bucket and active-upload opt-ins, plus `files-deleted` and `summary-json` reporting for canceled, skipped, and failed uploads. ([#98](https://github.com/backblaze-labs/b2-action/issues/98))

## [1.1.0] - 2026-06-23

### Security
Expand Down
9 changes: 5 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ flowchart LR
D --> R["retention"]
D --> Hd["head"]
D --> Pg["purge"]
D --> Cu["cleanup-unfinished"]

U & Dn & S & Cp & Dl & P & L & H & V & R & Hd & Pg --> SDK["@backblaze-labs/b2-sdk"]
U & Dn & S & Cp & Dl & P & L & H & V & R & Hd & Pg & Cu --> SDK["@backblaze-labs/b2-sdk"]
SDK --> B2[("Backblaze B2")]

U & Dn & S & Cp & Dl & P & L & H & V & R & Hd & Pg --> O["outputs +<br/>$GITHUB_STEP_SUMMARY"]
U & Dn & S & Cp & Dl & P & L & H & V & R & Hd & Pg & Cu --> O["outputs +<br/>$GITHUB_STEP_SUMMARY"]

style M fill:#EE3232,stroke:#fff,color:#fff
style SDK fill:#EE3232,stroke:#fff,color:#fff
Expand All @@ -47,7 +48,7 @@ src/
progress.ts # throttled progress listener
summary.ts # $GITHUB_STEP_SUMMARY writer
version.ts # VERSION constant (bumped in lockstep with package.json)
commands/<verb>.ts # one file per verb (13 today)
commands/<verb>.ts # one file per verb (14 today)
__tests__/
_helpers.ts # shared makeInputs() builder for command tests
*.test.ts # unit tests against the SDK's in-memory B2Simulator
Expand Down Expand Up @@ -281,7 +282,7 @@ Apply this to each bucket (the satellite ones get the same treatment as the main

Create one application key with these capabilities, scoped to the three buckets (or to "all buckets" if you prefer the simpler scope and accept the broader blast radius):

- `listBuckets`, `listFiles`, `readFiles`, `writeFiles`, `deleteFiles`: needed by `upload`, `download`, `sync`, `list`, `delete`, `copy`, `hide`, `unhide`, `purge`.
- `listBuckets`, `listFiles`, `readFiles`, `writeFiles`, `deleteFiles`: needed by `upload`, `download`, `sync`, `list`, `delete`, `copy`, `hide`, `unhide`, `purge`, `cleanup-unfinished`.
- `readFileRetentions`, `writeFileRetentions`, `readFileLegalHolds`, `writeFileLegalHolds`: needed by the `retention` example.
- `bypassGovernance`: only if you want the test that exercises shortening a governance retention.
- `shareFiles`: needed by `presign`.
Expand Down
51 changes: 44 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

[![CI](https://github.com/backblaze-labs/b2-action/actions/workflows/ci.yml/badge.svg)](https://github.com/backblaze-labs/b2-action/actions/workflows/ci.yml) [![Release](https://github.com/backblaze-labs/b2-action/actions/workflows/release.yml/badge.svg)](https://github.com/backblaze-labs/b2-action/actions/workflows/release.yml) [![Marketplace](https://img.shields.io/github/v/release/backblaze-labs/b2-action?label=marketplace&color=red&logo=githubactions&logoColor=white)](https://github.com/marketplace/actions/backblaze-b2-cloud-storage-action) [![Latest release](https://img.shields.io/github/v/release/backblaze-labs/b2-action?display_name=tag&sort=semver&color=blue)](https://github.com/backblaze-labs/b2-action/releases/latest) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](./LICENSE) [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](./vitest.config.ts) [![Docs](https://img.shields.io/github/deployments/backblaze-labs/b2-action/github-pages?label=docs&logo=readthedocs&logoColor=white)](https://backblaze-labs.github.io/b2-action/)

A Backblaze-maintained B2 GitHub Action. TypeScript-native, built on [@backblaze-labs/b2-sdk](https://github.com/backblaze-labs/b2-sdk-typescript). Thirteen verbs covering every B2 operation a CI workflow needs. Currently incubating in [Backblaze Labs](https://github.com/backblaze-labs).
A Backblaze-maintained B2 GitHub Action. TypeScript-native, built on [@backblaze-labs/b2-sdk](https://github.com/backblaze-labs/b2-sdk-typescript). Fourteen verbs covering every B2 operation a CI workflow needs. Currently incubating in [Backblaze Labs](https://github.com/backblaze-labs).

- **Node 24 action.** No Docker. Sub-second cold start.
- **Thirteen verbs.** `upload`, `download`, `sync`, `copy`, `delete`, `list`, `hide`, `unhide`, `verify`, `presign`, `retention`, `head`, `purge`: pick via the `action` input.
- **Fourteen verbs.** `upload`, `download`, `sync`, `copy`, `delete`, `list`, `hide`, `unhide`, `verify`, `presign`, `retention`, `head`, `purge`, `cleanup-unfinished`: pick via the `action` input.
- **Resumable multipart uploads** for any file size; streaming I/O so multi-GB payloads don't buffer in RAM.
- **Server-side everything.** `copy` (same-bucket or cross-bucket) and `delete` operations stay server-side; bytes never traverse the runner.
- **Server-side encryption.** SSE-B2 (managed) and SSE-C (customer key, base64).
Expand All @@ -31,6 +31,7 @@ A Backblaze-maintained B2 GitHub Action. TypeScript-native, built on [@backblaze
- [Sync (both directions)](#sync-both-directions)
- [Server-side copy (same-bucket or cross-bucket)](#server-side-copy-same-bucket-or-cross-bucket)
- [List, dry-run-delete, delete](#list-dry-run-delete-delete)
- [Cleanup unfinished multipart uploads](#cleanup-unfinished-multipart-uploads)
- [Hide / unhide](#hide--unhide)
- [Verify SHA-1 without downloading](#verify-sha-1-without-downloading)
- [Presign a download URL](#presign-a-download-url)
Expand Down Expand Up @@ -101,6 +102,7 @@ Exact-version releases publish an attested `dist/index.js` asset for provenance
| `retention` | Apply Object Lock retention + legal hold to a file. | `source`, `bucket`, plus `retention-mode` and/or `legal-hold` |
| `head` | Fetch object metadata (size, sha1, contentType, fileInfo) via HEAD. No body transfer. | `source`, `bucket` |
| `purge` | Permanently delete every file version under a prefix, including hide markers and history. Whole-bucket purge requires `allow-bucket-purge: true`. Supports `dry-run` and `bypass-governance` for governance-retained versions. | `source` or `allow-bucket-purge`, `bucket` |
| `cleanup-unfinished` | List unfinished multipart uploads under a prefix and cancel idle uploads so uploaded parts stop being retained. Supports `dry-run`; non-dry-run whole-bucket cleanup requires `allow-bucket-cleanup: true`, and active or diagnostically unknown uploads are skipped unless `cleanup-unfinished-force: true`. | `bucket` (and usually `source`) |

Exact-name `copy`, single-file `delete`, and `retention` operate only when the latest exact-name version is an upload. If that latest version is a hide marker, these commands do not search older upload history under the same name; they fail with the same `File not found` diagnostic used for absent names so default workflow logs do not reveal hidden-object existence. Run `unhide` first to restore the prior upload, or use `purge` when you need to remove hide markers and historical versions.

Expand Down Expand Up @@ -240,6 +242,36 @@ Exact-name `copy`, single-file `delete`, and `retention` operate only when the l
dry-run: true
```

### Cleanup unfinished multipart uploads

```yaml
- id: cleanup-preview
uses: backblaze-labs/b2-action@v1
with:
action: cleanup-unfinished
bucket: my-bucket
source: tmp/
dry-run: true

- uses: backblaze-labs/b2-action@v1
with:
action: cleanup-unfinished
bucket: my-bucket
source: tmp/
```

By default, `cleanup-unfinished` only cancels uploads whose newest uploaded
part is at least 1440 minutes old. Uploads with recent parts, no uploaded
parts, failed part diagnostics, or more than the 100-part diagnostic cap are
reported as skipped so active multipart uploads are not canceled accidentally.
Set `cleanup-unfinished-force: true` to cancel those matched uploads anyway.
Comment thread
goanpeca marked this conversation as resolved.
For non-dry-run whole-bucket cleanup, omit `source` or set `/` and also set
`allow-bucket-cleanup: true`.

Cleanup part diagnostics are best-effort. `partCount` and `size` can be `null`
when part inspection fails, and when `partsTruncated` is true the reported
counts and bytes are lower bounds from the first 100 scanned parts.

### Hide / unhide

```yaml
Expand Down Expand Up @@ -369,12 +401,12 @@ Set `bypass-governance: true` to shorten governance-mode retention or to remove

| Input | Required | Default | Description |
| --- | --- | --- | --- |
| `action` | yes | | One of 13: `upload`, `download`, `sync`, `copy`, `delete`, `presign`, `list`, `hide`, `unhide`, `verify`, `retention`, `head`, `purge` |
| `action` | yes | | One of 14: `upload`, `download`, `sync`, `copy`, `delete`, `presign`, `list`, `hide`, `unhide`, `verify`, `retention`, `head`, `purge`, `cleanup-unfinished` |
| `application-key-id` | no\* | | B2 application key ID. Falls back to `$B2_APPLICATION_KEY_ID`. |
| `application-key` | no\* | | B2 application key. Falls back to `$B2_APPLICATION_KEY`. |
| `bucket` | yes | | Destination bucket name. |
| `source-bucket` | copy only | `bucket` | Source bucket for cross-bucket copy. |
| `source` | command-dependent | | Local path/glob (upload/sync up); B2 file name or prefix (everything else). Prefix downloads reject keys with empty, `.`, `..`, or control-character path segments. For whole-bucket purge, omit `source` or set `/` and set `allow-bucket-purge: true`. |
| `source` | command-dependent | | Local path/glob (upload/sync up); B2 file name or prefix (everything else). For `cleanup-unfinished`, this is an optional unfinished upload prefix; non-dry-run whole-bucket cleanup requires `allow-bucket-cleanup: true`. Prefix downloads reject keys with empty, `.`, `..`, or control-character path segments. For whole-bucket purge, omit `source` or set `/` and set `allow-bucket-purge: true`. |
| `destination` | command-dependent | | B2 file/prefix (upload/sync up/copy); local path (download/sync down/verify). Upload destinations are not normalized by the action; SDK/B2 key validation errors are surfaced rather than silently rewriting `/` characters. |
| `include` | no | | CSV of glob patterns to include (upload). |
| `exclude` | no | `.git/**` | CSV of glob patterns to exclude (upload). |
Expand All @@ -388,8 +420,11 @@ Set `bypass-governance: true` to shorten governance-mode retention or to remove
| `content-language` | no | | Content-Language response header to store with uploaded files. |
| `expires` | no | | Expires response header to store with uploaded files. |
| `preserve-mtime` | no | `false` | Store each uploaded file's local modification time as B2 `src_last_modified_millis`. |
| `dry-run` | no | `false` | Preview only (sync/delete/purge). |
| `dry-run` | no | `false` | Preview only (sync/delete/purge/cleanup-unfinished). |
| `allow-bucket-purge` | purge only | `false` | Permit `purge` to target the entire bucket when `source` is empty or `/`. |
| `allow-bucket-cleanup` | cleanup-unfinished only | `false` | Permit non-dry-run `cleanup-unfinished` to target the entire bucket when `source` is empty or `/`; ignored by other actions. |
| `cleanup-unfinished-force` | cleanup-unfinished only | `false` | Cancel active or diagnostically unknown unfinished uploads matched by `source`. |
| `cleanup-unfinished-idle-minutes` | cleanup-unfinished only | `1440` | Minimum minutes since the newest uploaded part before cleanup-unfinished cancels by default. |
| `presign-ttl` | no | `3600` | Presigned URL TTL in seconds. Must be a positive decimal integer. |
| `endpoint` | no | | Override B2 realm (staging/custom). |
| `fail-on-empty` | no | `true` | Fail if an upload glob matches zero files. |
Expand All @@ -414,10 +449,10 @@ Set `bypass-governance: true` to shorten governance-mode retention or to remove
| `file-name` | single-file ops | B2 file name (path). |
| `content-sha1` | upload / download / head when available | SHA-1 hex. Omitted when B2 does not expose a whole-file SHA-1, including multipart objects. |
| `bytes-transferred` | upload / download / sync / copy / head | Total bytes moved. Head emits `0`. |
| `file-count` | every command | Aggregate count of files matched or processed, including skipped sync entries and dry-run delete/purge matches. Prefer verb-specific count outputs when available. |
| `file-count` | every command | Aggregate count of files matched or processed, including skipped sync entries and dry-run delete/purge/cleanup-unfinished matches. Prefer verb-specific count outputs when available. |
| `files-uploaded` | upload / sync up | Count. |
| `files-downloaded` | download / sync down | Count. |
| `files-deleted` | delete / purge / sync | Count. |
| `files-deleted` | delete / purge / sync / cleanup-unfinished | Count. For `cleanup-unfinished`, this is the count of unfinished large uploads canceled; dry-run previews report `0`. |
| `files-listed` | list / prefix presign | Count returned (capped by `max-results`). |
| `presigned-url` | presign | Time-limited download URL. Masked as a secret. This is the only structured output that carries the live presigned URL. |
| `verified` | verify | `true` / `false`. |
Expand All @@ -440,6 +475,8 @@ For every command, `summary-json` and `summary-json-preview` omit fields with cr

For `presign`, `summary-json` and `summary-json-preview` contain only non-secret manifest fields such as `fileName` and `expiresAt`; the dedicated `presigned-url` output is the only structured output that contains a bearer URL. In prefix mode, only the first generated URL is exposed through `presigned-url`; bulk URL fan-out through `summary-json` is intentionally unsupported because those URLs are credentials. Generate or handle additional URLs in a trusted step that treats them as secrets.

For `cleanup-unfinished`, `summary-json` and `summary-json-preview` are projected to cleanup status fields (`fileName`, `fileId`, `partCount`, `size`, `partsTruncated`, `status`, `reason`, and sanitized `error`). B2 `fileInfo` metadata is never emitted for this command. `partCount` and `size` are best-effort diagnostics: they can be `null` when part inspection fails, and when `partsTruncated` is true they are lower bounds from the first 100 scanned parts.

`$GITHUB_STEP_SUMMARY` per-file tables render at most the first 100 rows. When more rows exist, the summary includes a `Showing first 100 of N rows.` notice and the scalar outputs keep reporting the full source count. Status cells are escaped and rendered as inline code so object metadata cannot break the markdown table.

`retryable` and `retry-after` are emitted only on the failure path, immediately before the Action calls `core.setFailed`. To consume them, set `continue-on-error: true` on the B2 step and guard the follow-up step explicitly:
Expand Down
3 changes: 3 additions & 0 deletions __tests__/_parsed-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export function makeParsedInputs(
preserveMtime: false,
dryRun: false,
allowBucketPurge: false,
allowBucketCleanup: false,
cleanupUnfinishedForce: false,
cleanupUnfinishedIdleMinutes: 24 * 60,
presignTtlSeconds: 3600,
endpoint: undefined,
failOnEmpty: true,
Expand Down
Loading
Loading