Skip to content
Merged
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
35 changes: 35 additions & 0 deletions .github/workflows/test-key-value-store.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Test key-value store actions

on:
pull_request:
paths:
- key-value-store/**
- .github/workflows/test-key-value-store.yml
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
name: Node 24 (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: '24'
package-manager-cache: false
- name: Test client and action entrypoints
run: node --test key-value-store/tests/client.test.cjs key-value-store/tests/entrypoints.test.cjs
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Shared GitHub Actions for Astral projects.

- [Key-value store](key-value-store/): share workflow state within a GitHub Actions
OIDC namespace.
- [Rust workspace cache](rust-workspace-cache/): cache workspace crates and prune
superseded artifacts before saving.
- [Create release pull request](create-release-pr/): commit prepared release files
Expand Down
168 changes: 168 additions & 0 deletions key-value-store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Key-value store

Store workflow state with `key-value-store/put`, retrieve it with
`key-value-store/get`, and remove it with `key-value-store/delete`. Each action
requests a fresh GitHub Actions OIDC token and exchanges it for a short-lived
session with the configured service. No static secret or separate login step is
required.

The service derives the namespace from the signed OIDC context. Jobs and workflow
runs for the same pull request and environment can share keys. Different pull
requests receive separate namespaces, as do different repositories, events,
refs, and environments. A branch push and a pull request do not share keys, even
for the same commit. There is no input to select another namespace.

## Usage

Configure `KEY_VALUE_SERVICE_URL` as a repository variable containing the HTTPS
origin of a compatible service, such as `https://kv.example.com`. Grant
`id-token: write` to each job that uses these actions. Pin all three actions to
the same full commit SHA.

```yaml
name: Workflow state
on: pull_request

permissions:
id-token: write

jobs:
state:
runs-on: ubuntu-latest
steps:
- uses: astral-sh/github-actions/key-value-store/put@<commit-sha>
with:
service-url: ${{ vars.KEY_VALUE_SERVICE_URL }}
key: build-sha
value: ${{ github.sha }}

- uses: astral-sh/github-actions/key-value-store/get@<commit-sha>
id: stored
with:
service-url: ${{ vars.KEY_VALUE_SERVICE_URL }}
key: build-sha

- name: Compare the stored commit
if: steps.stored.outputs.found == 'true'
env:
STORED_SHA: ${{ steps.stored.outputs.value }}
run: test "$STORED_SHA" = "$GITHUB_SHA"

- uses: astral-sh/github-actions/key-value-store/delete@<commit-sha>
with:
service-url: ${{ vars.KEY_VALUE_SERVICE_URL }}
key: build-sha
```

The actions run on Linux, macOS, and Windows with a runner that supports Node 24
actions. If the runner uses `HTTP_PROXY` or `HTTPS_PROXY`, set
`NODE_USE_ENV_PROXY: '1'` in the job environment.

## Inputs and outputs

All three actions accept these inputs:

| Input | Default | Description |
| --- | --- | --- |
| `service-url` | Required | HTTPS origin, without credentials, a path, a query, or a fragment. |
| `key` | Required | Case-sensitive key: 1–256 ASCII letters, digits, `.`, `_`, or `-`; cannot be `.` or `..`. |
| `audience` | The `service-url` origin | Audience requested from GitHub. Must match the service configuration; at most 2,048 UTF-8 bytes without whitespace. |

`put` also accepts:

| Input | Default | Description |
| --- | --- | --- |
| `value` | Empty | UTF-8 text to store, including its whitespace and newlines. |
| `path` | Empty | Regular file whose raw bytes are stored instead of `value`. Cannot be combined with a nonempty `value`. |

Values may contain up to 65,536 bytes. Omitting both `value` and `path` stores an
empty value. Writing an existing key replaces its value and resets its expiry.
Concurrent writes are last-write-wins; their order is not guaranteed. The service
sets the retention period. These actions do not choose a TTL.

`get` also accepts:

| Input | Default | Description |
| --- | --- | --- |
| `path` | Empty | New file for the raw bytes. The file must not already exist; parent directories are created as needed. |
| `mask-value` | `true` | Mask a retrieved text value in runner logs. Set `false` only for nonsensitive values. Does not apply to file downloads. |

`get` produces these outputs:

| Output | Description |
| --- | --- |
| `found` | `true` for an unexpired value, including an empty value; `false` for a missing or expired key. |
| `value` | UTF-8 text when `path` is omitted; otherwise empty. |
| `path` | Absolute path of the file created; empty when no file was written. |
| `expires-at` | Value expiry in Unix seconds; empty when the key was not found. |

A missing key succeeds with `found: 'false'`, empty remaining outputs, and no
file. Text mode rejects invalid UTF-8 and NUL bytes; use `path` for binary data.
GitHub may suppress masked values passed through job outputs, so use
`mask-value: 'false'` only when a nonsensitive value must cross that boundary.
Masking does not encrypt a value or prevent a later step from accessing it.

`put` and `delete` have no outputs. Deleting a missing key succeeds. Other service
errors fail the action. Requests have a 15-second timeout and are not retried;
after a network failure, a write may already have reached the service.

## Files

Use `path` to transfer raw bytes without putting the value in an action output:

```yaml
- uses: astral-sh/github-actions/key-value-store/put@<commit-sha>
with:
service-url: ${{ vars.KEY_VALUE_SERVICE_URL }}
key: build-result
path: ${{ runner.temp }}/build-result.bin

- uses: astral-sh/github-actions/key-value-store/get@<commit-sha>
id: restored
with:
service-url: ${{ vars.KEY_VALUE_SERVICE_URL }}
key: build-result
path: ${{ runner.temp }}/restored-result.bin
```

Paths may be absolute or relative to the job's working directory. The input file
must already exist and cannot itself be a symlink. The output file is created
exclusively, with owner-only permissions where supported. Paths and their parent
directories must be trusted: parent symlinks are followed, and these actions do
not sandbox filesystem access. Prefer a fresh path under `runner.temp` for
downloads, outside directories controlled by pull request contents.

## Service API

| Request | Authentication | Successful response |
| --- | --- | --- |
| `POST /exchange`, empty body | GitHub OIDC bearer token | JSON with `access_token`, `token_type: "Bearer"`, and `expires_at` in Unix seconds. |
| `PUT /kv/<key>`, raw bytes | Session bearer token | `204`. |
| `GET /kv/<key>` | Session bearer token | `200` with raw bytes and `X-Expires-At` in Unix seconds, or `404`. |
| `DELETE /kv/<key>` | Session bearer token | `204`, including for an absent key. |

Session tokens use the format `aghs_kv_` followed by 64 lowercase hexadecimal
characters. They remain internal to the action and are never action outputs.
Each invocation requests a new OIDC token because the service allows each OIDC
token to be exchanged once. No service deployment is included with these actions.

## Trust boundaries

The configured service receives the OIDC token and stored values. Keep
`service-url`, `audience`, and file paths in trusted workflow configuration;
do not derive them from pull request content. HTTPS certificate verification is
enabled by default, redirects are rejected, and response bodies are bounded.
OIDC and session tokens are masked before further requests, and error responses
are not printed.

Namespace isolation is enforced by the service. All actors, jobs, and workflows
within the same accepted OIDC context share read, write, and delete access. This
is not a secret store for mutually untrusted jobs in one pull request. The
service supports `pull_request`, `push`, `workflow_dispatch`, and `schedule`
contexts; it rejects `pull_request_target` and `workflow_run`. Adding an
environment changes the namespace. Sessions are bearer credentials: anyone
holding one can access its namespace until the session expires.

Treat retrieved values as untrusted data. Pass text through `env` and quote shell
variables, as above, or consume downloaded files as data. Do not interpolate a
retrieved `value` directly into a `run` script or execute downloaded contents.
Loading
Loading