Skip to content

Commit 15b77b6

Browse files
0 parents  commit 15b77b6

22 files changed

Lines changed: 2389 additions & 0 deletions

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 primitive.dev
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Deploy Primitive Function
2+
3+
GitHub Action that deploys a [Primitive Function](https://primitive.dev) to primitive.dev. Idempotent — creates the function on first run, updates it on subsequent runs. Manages custom `function_secrets` in lockstep with the deploy and triggers a re-bind when bindings change.
4+
5+
## Quick start
6+
7+
```yaml
8+
- uses: actions/checkout@v4
9+
- run: pnpm install --frozen-lockfile && pnpm build
10+
- uses: primitivedotdev/deploy-function@v1
11+
with:
12+
api-key: ${{ secrets.PRIMITIVE_API_KEY }}
13+
name: my-function
14+
code-path: dist/handler.js
15+
source-map-path: dist/handler.js.map
16+
```
17+
18+
## Inputs
19+
20+
| Input | Required | Default | Description |
21+
|---|---|---|---|
22+
| `api-key` | yes | — | Org-scoped Primitive API key. Pass via `${{ secrets.* }}` — masked in logs. |
23+
| `api-base-url` | no | `https://api.primitive.dev/v1` | API base URL. Override only if deploying against a non-production environment. |
24+
| `name` | yes | — | Function name. `/^[a-z0-9_-]{1,64}$/`. Idempotent within the org. |
25+
| `code-path` | one of | — | Path to a pre-built ESM bundle (e.g. `dist/handler.js`). Mutually exclusive with `files-path`. |
26+
| `source-map-path` | no | — | Source map for the `code-path` bundle. Surfaces readable stack traces in the dashboard. |
27+
| `files-path` | one of | — | Path to a source directory for **managed build**. The Action walks the tree, applies ignore patterns, and the platform builds server-side. Directory must contain a `package.json` at its root. Requires the `functions_managed_build` entitlement on the org. Mutually exclusive with `code-path`. |
28+
| `ignore` | no | — | Newline-delimited basename patterns to skip when walking `files-path`. Layered on top of the [default ignore list](#default-ignore-list). |
29+
| `secrets` | no | `{}` | JSON object of custom function secrets to upsert. Values are masked. |
30+
| `redeploy-on-secret-change` | no | `true` | Re-bind the runtime after upserting secrets. |
31+
| `expected-org-id` | no | — | Safety guard: aborts if the API key's org differs from this UUID. Strongly recommended in production workflows. |
32+
33+
Exactly one of `code-path` and `files-path` must be set.
34+
35+
### Default ignore list
36+
37+
`node_modules`, `.git`, `.github`, `dist`, `build`, `.next`, `.turbo`, `.vercel`, `coverage`, `.DS_Store`, `.env`, `.env.local`, `.env.*` (covers `.env.production`, `.env.staging`, etc).
38+
39+
Add to it via the `ignore` input — basenames anywhere in the tree, exact match or simple glob (`*.log`, `*.test.ts`). `#` comments and blank lines allowed.
40+
41+
## Outputs
42+
43+
| Output | Description |
44+
|---|---|
45+
| `function-id` | UUID of the created or updated function. |
46+
| `deploy-status` | `deployed`, `pending`, or `failed`. |
47+
| `created` | `true` on initial create, `false` on update. |
48+
49+
## Examples
50+
51+
### Minimal — pre-built bundle
52+
53+
```yaml
54+
- uses: primitivedotdev/deploy-function@v1
55+
with:
56+
api-key: ${{ secrets.PRIMITIVE_API_KEY }}
57+
name: my-function
58+
code-path: dist/handler.js
59+
```
60+
61+
### With custom secrets and an org-id guard
62+
63+
```yaml
64+
- uses: primitivedotdev/deploy-function@v1
65+
with:
66+
api-key: ${{ secrets.PRIMITIVE_API_KEY }}
67+
expected-org-id: ${{ vars.PRIMITIVE_ORG_ID }}
68+
name: my-function
69+
code-path: dist/handler.js
70+
secrets: |
71+
{
72+
"OPENAI_API_KEY": "${{ secrets.OPENAI_API_KEY }}",
73+
"FEATURE_FLAG": "on"
74+
}
75+
```
76+
77+
The action upserts each secret via `POST /v1/functions/{id}/secrets`, then calls `POST /v1/functions/{id}/redeploy` so the new bindings are live in the runtime. Set `redeploy-on-secret-change: false` to skip the redeploy step (only the secret rows are touched).
78+
79+
### Managed build (`files-path`)
80+
81+
Skip the build step and hand the platform your source. The platform bundles server-side, applying the same Workers-for-Platforms compatibility flags as the dashboard's managed-build path.
82+
83+
```yaml
84+
- uses: actions/checkout@v4
85+
- uses: primitivedotdev/deploy-function@v1
86+
with:
87+
api-key: ${{ secrets.PRIMITIVE_API_KEY }}
88+
name: my-function
89+
files-path: ./function-source
90+
ignore: |
91+
# local-only assets the platform doesn't need
92+
fixtures
93+
*.test.ts
94+
*.spec.ts
95+
```
96+
97+
`function-source/` must contain a `package.json` at its root (dependencies may be empty).
98+
99+
### Using outputs
100+
101+
```yaml
102+
- id: deploy
103+
uses: primitivedotdev/deploy-function@v1
104+
with:
105+
api-key: ${{ secrets.PRIMITIVE_API_KEY }}
106+
name: my-function
107+
code-path: dist/handler.js
108+
- run: echo "Deployed function-id=${{ steps.deploy.outputs.function-id }} (created=${{ steps.deploy.outputs.created }})"
109+
```
110+
111+
## Security
112+
113+
- The `api-key` input is automatically masked. Pass it as a GitHub secret (`${{ secrets.* }}`) — never hard-code.
114+
- Every value passed via `secrets` is also masked.
115+
- Use `expected-org-id` in production workflows. It calls `GET /v1/whoami` before any write and aborts if the API key's org doesn't match.
116+
117+
## Versioning
118+
119+
- Floating major tag `v1` always tracks the latest 1.x.
120+
- Pin a specific minor/patch (`v1.2.0`) for reproducible deploys.
121+
- Breaking changes bump the major; `v1` stays alive for a deprecation window.
122+
123+
## Source
124+
125+
This action is authored in [primitivedotdev/primitive-mono-repo](https://github.com/primitivedotdev/primitive-mono-repo) under `tools/actions/deploy-function/` and mirrored here on release tags. See [RELEASING.md](./RELEASING.md) for the release process.
126+
127+
## License
128+
129+
MIT — see [LICENSE](./LICENSE).

RELEASING.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Releasing `deploy-function`
2+
3+
This Action is authored in [`primitivedotdev/primitive-mono-repo`](https://github.com/primitivedotdev/primitive-mono-repo) under `tools/actions/deploy-function/` and **mirrored on tag** to the public repo [`primitivedotdev/deploy-function`](https://github.com/primitivedotdev/deploy-function). External consumers pin a floating major (`@v1`) or an immutable patch (`@v1.2.3`) from the public repo.
4+
5+
The mirror is **one-way**: every release wipes the public tree and force-replaces it with the contents of `tools/actions/deploy-function/`. No PRs are accepted against the public repo.
6+
7+
## One-time setup
8+
9+
Done once when this Action goes public.
10+
11+
### 1. Create the public repo
12+
13+
```bash
14+
gh repo create primitivedotdev/deploy-function \
15+
--public \
16+
--description "GitHub Action for deploying a Primitive Function — primitive.dev" \
17+
--homepage https://primitive.dev
18+
```
19+
20+
Leave the repo empty — the first tag push from this monorepo populates it.
21+
22+
### 2. (Nothing) — the App auth is already wired up
23+
24+
The mirror workflow authenticates as the `primitive-ci` GitHub App, the same App used by `sync-staging-to-main`. The App is installed at the **org level with `repository_selection: "all"`**, so `deploy-function` is covered automatically the moment the repo is created — no per-repo install step is needed. The App credential is read at run time from AWS Secrets Manager (`staging/github-ci-app`) via the existing OIDC role; no GitHub Actions secret to manage.
25+
26+
If `repository_selection` ever changes to `selected` in the future, add `deploy-function` to the install's repository list.
27+
28+
## Cutting a release
29+
30+
1. **Confirm the change is on `main`** — the mirror workflow asserts the tag commit is reachable from `origin/main` and refuses to publish otherwise.
31+
2. **Pick a version** following semver (`vX.Y.Z`). Breaking changes bump the major.
32+
3. **Tag locally and push**:
33+
34+
```bash
35+
git checkout main
36+
git pull --ff-only
37+
git tag deploy-function-action-v1.2.3
38+
git push origin deploy-function-action-v1.2.3
39+
```
40+
41+
4. **Watch the mirror run**:
42+
43+
```bash
44+
gh run watch \
45+
"$(gh run list --workflow mirror-deploy-function-action.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
46+
```
47+
48+
5. **Confirm the public repo updated**:
49+
50+
```bash
51+
# The mirror only pushes git tags (not GitHub Releases), so verify
52+
# against the tag list directly.
53+
gh api repos/primitivedotdev/deploy-function/tags --jq '.[].name'
54+
curl -fsSL https://raw.githubusercontent.com/primitivedotdev/deploy-function/v1/action.yml | head -20
55+
```
56+
57+
## How `vMAJOR` works
58+
59+
The mirror always force-pushes a floating major tag (`v1`) pointing at the latest `v1.x.y`. Consumers who pin `@v1` get the newest patch automatically. Consumers who pin `@v1.2.3` get exactly that immutable version.
60+
61+
When you ship `v2.0.0`, the workflow creates `v2` (new floating major) without touching `v1` — so existing `@v1` consumers stay on the v1 line until they explicitly upgrade.
62+
63+
## Rollback
64+
65+
If a release breaks consumers in production:
66+
67+
1. Tag the **previous** good commit with a new patch version that supersedes the broken one (don't try to "delete" the broken tag; consumers may have already pinned it):
68+
69+
```bash
70+
git tag deploy-function-action-v1.2.4 <previous-good-sha>
71+
git push origin deploy-function-action-v1.2.4
72+
```
73+
74+
The mirror will rebuild the public repo from that commit and move `v1` to point at `v1.2.4`.
75+
76+
2. If the breakage is in the bundled `dist/`, you can also just **revert + re-tag** on the monorepo, since `dist/` regenerates from `src/` on every build (the `check-dist.mjs` CI guard ensures parity).

action.yml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
name: Deploy Primitive Function
2+
description: >-
3+
Deploys a Primitive Function to primitive.dev via the public API.
4+
Idempotent: creates the function on first run, updates the bundle
5+
on subsequent runs. Manages custom function_secrets in lockstep
6+
with the deploy and triggers a redeploy when bindings change.
7+
author: primitive.dev
8+
9+
branding:
10+
icon: send
11+
color: blue
12+
13+
inputs:
14+
api-key:
15+
description: >-
16+
Primitive API key. Org-scoped; must belong to the org you are
17+
deploying into. Pass via a GitHub secret reference (see README
18+
for the usage example) — never hard-code. Masked in logs.
19+
required: true
20+
api-base-url:
21+
description: >-
22+
Primitive API base URL. Defaults to production. Set to a
23+
non-production URL if you are deploying against a custom
24+
environment.
25+
required: false
26+
default: https://api.primitive.dev/v1
27+
name:
28+
description: >-
29+
Function name. Lowercase letters, digits, dashes, underscores;
30+
max 64 chars. Idempotent within the org.
31+
required: true
32+
code-path:
33+
description: >-
34+
Path to the pre-built ESM bundle (e.g. `dist/handler.js`). The
35+
build is the caller's responsibility; this Action only uploads.
36+
Exactly one of `code-path` and `files-path` is required.
37+
required: false
38+
source-map-path:
39+
description: >-
40+
Optional path to the bundle's source map. When present, surfaces
41+
readable stack traces in the dashboard's invocation logs.
42+
Only valid with `code-path`.
43+
required: false
44+
files-path:
45+
description: >-
46+
Path to a source directory for managed build mode. The Action
47+
walks the directory, applies the default + caller ignore
48+
patterns, and sends the resulting `{relative-path: contents}`
49+
map as the `files` field. The directory MUST contain a
50+
`package.json` at the root. Requires the
51+
`functions_managed_build` entitlement on the org. Exactly one
52+
of `code-path` and `files-path` is required.
53+
required: false
54+
ignore:
55+
description: >-
56+
Additional ignore patterns layered on top of the default list
57+
(`node_modules`, `.git`, `.github`, `dist`, `build`, `.next`,
58+
`.turbo`, `.vercel`, `coverage`, `.DS_Store`, `.env`,
59+
`.env.local`, `.env.*`). One pattern per line. Each pattern is
60+
matched against basenames anywhere in the tree. Supports exact
61+
names (`secrets.txt`) or simple globs (`*.log`, `*.test.ts`).
62+
`#` comments and blank lines allowed. Only used with
63+
`files-path`.
64+
required: false
65+
default: ''
66+
secrets:
67+
description: >-
68+
JSON object of custom function_secrets to upsert before deploy.
69+
Values are masked in logs. Example:
70+
`{"OPENAI_API_KEY":"sk-…","FEATURE_FLAG":"on"}`. Pass an empty
71+
object (or omit) to leave existing secrets untouched.
72+
required: false
73+
default: '{}'
74+
redeploy-on-secret-change:
75+
description: >-
76+
When `true` and at least one secret was upserted, call
77+
`/v1/functions/{id}/redeploy` after the upsert so the new
78+
bindings are wired into the runtime. Default `true`. Safe to
79+
leave on; the redeploy is a no-op when nothing changed.
80+
required: false
81+
default: 'true'
82+
expected-org-id:
83+
description: >-
84+
Optional safety guard. When set, the Action calls `/v1/whoami`
85+
before any write and aborts if the API key's org differs from
86+
this value. Strongly recommended in production workflows to
87+
prevent a leaked-key cross-org incident.
88+
required: false
89+
90+
outputs:
91+
function-id:
92+
description: UUID of the created or updated function.
93+
deploy-status:
94+
description: '`deployed`, `pending`, or `failed` (the platform-reported state after this run).'
95+
created:
96+
description: '`true` on initial create, `false` on update.'
97+
98+
runs:
99+
using: node20
100+
main: dist/index.js

dist/index.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)