Skip to content

Commit 2cb0161

Browse files
committed
[release] Add GitHub Actions for appwrite-utils-cli (deploy-functions, push-schema, generic)
Composite actions that install the CLI via bunx instead of pnpm, so esbuild's build script runs non-interactively and CI never hangs on the pnpm "Choose which packages to build" prompt. Backed by a shared, dependency-free run.ts that bun executes directly (no build step). Credentials forwarded via env only, never on argv.
1 parent b797c23 commit 2cb0161

5 files changed

Lines changed: 462 additions & 0 deletions

File tree

actions/README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Appwrite Utils GitHub Actions
2+
3+
Reusable composite actions that run [`appwrite-utils-cli`](https://www.npmjs.com/package/appwrite-utils-cli)
4+
in CI. They install and run the CLI via **`bunx`**, which executes dependency build
5+
scripts (esbuild, pulled in transitively by the CLI's runtime `tsx` dependency)
6+
**non-interactively** — so there is no `pnpm`-style "Choose which packages to build"
7+
prompt to hang your pipeline.
8+
9+
All three actions share one dependency-free orchestration script
10+
(`actions/_shared/run.ts`) that bun runs directly — no build step, no bundled
11+
`dist/`. The Appwrite API key is forwarded to the CLI through the environment only,
12+
never on the command line, so it never appears in a logged command.
13+
14+
## Actions
15+
16+
| Action | CLI command | Purpose |
17+
| --- | --- | --- |
18+
| `actions/deploy-functions` | `--deployFunctions` | Deploy one or more Appwrite Functions. |
19+
| `actions/push-schema` | `--push` | Deploy local config (tables, columns, indexes) to Appwrite. |
20+
| `actions/appwrite-migrate` | _(raw args)_ | Run `appwrite-migrate` with any arguments (sync, import, generate, backup, regen, …). |
21+
22+
## Deploy functions (matrix example)
23+
24+
```yaml
25+
jobs:
26+
deploy:
27+
strategy:
28+
matrix:
29+
fn: [fn-a, fn-b, fn-c]
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
- uses: zachhandley/AppwriteUtils/actions/deploy-functions@dev
34+
with:
35+
endpoint: ${{ secrets.APPWRITE_ENDPOINT }}
36+
project-id: ${{ secrets.APPWRITE_PROJECT_ID }}
37+
api-key: ${{ secrets.APPWRITE_API_KEY }}
38+
function-ids: ${{ matrix.fn }}
39+
```
40+
41+
Omit `function-ids` to deploy every function discovered from `.fnconfig.yaml`
42+
files and `config.yaml`'s `functions[]`.
43+
44+
## Push schema
45+
46+
```yaml
47+
- uses: zachhandley/AppwriteUtils/actions/push-schema@dev
48+
with:
49+
endpoint: ${{ secrets.APPWRITE_ENDPOINT }}
50+
project-id: ${{ secrets.APPWRITE_PROJECT_ID }}
51+
api-key: ${{ secrets.APPWRITE_API_KEY }}
52+
```
53+
54+
## Generic (any command)
55+
56+
```yaml
57+
- uses: zachhandley/AppwriteUtils/actions/appwrite-migrate@dev
58+
with:
59+
endpoint: ${{ secrets.APPWRITE_ENDPOINT }}
60+
project-id: ${{ secrets.APPWRITE_PROJECT_ID }}
61+
api-key: ${{ secrets.APPWRITE_API_KEY }}
62+
args: "--sync"
63+
```
64+
65+
## Common inputs
66+
67+
- `endpoint`, `project-id`, `api-key` — Appwrite credentials. The CLI also accepts
68+
`APPWRITE_ENDPOINT` / `APPWRITE_PROJECT_ID` / `APPWRITE_API_KEY`; these actions set
69+
those for you from the inputs.
70+
- `working-directory` (default `.`) — where the CLI runs (your config and function
71+
source live here; remember `actions/checkout` first).
72+
- `cli-version` (default `latest`) — the npm dist-tag or exact version of
73+
`appwrite-utils-cli` to run.
74+
75+
`deploy-functions` additionally exposes `function-ids`, `function-path`,
76+
`build-concurrency`, `config`, and single-function overrides (`runtime`,
77+
`entrypoint`, `commands`, `schedule`, `timeout`, `scopes`, `events`, `execute`,
78+
`enabled`, `logging`). The overrides only apply when exactly one function is
79+
targeted.
80+
81+
## Installing the CLI directly with pnpm (without these actions)
82+
83+
If you install `appwrite-utils-cli` yourself with **pnpm 10+**, pnpm will pause on
84+
an interactive prompt to approve `esbuild`'s build script. The old
85+
`pnpm.onlyBuiltDependencies` field in `package.json` is ignored by pnpm 10+; the
86+
allowlist now lives in `pnpm-workspace.yaml`:
87+
88+
```yaml
89+
# pnpm-workspace.yaml
90+
onlyBuiltDependencies:
91+
- esbuild
92+
```
93+
94+
Using the actions above avoids this entirely (they install via bun).

actions/_shared/run.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Shared orchestration script for the appwrite-utils GitHub Actions.
4+
*
5+
* Each composite action forwards its inputs as AWU_* environment variables and
6+
* invokes this script with `bun`. The script translates them into an
7+
* `appwrite-migrate` invocation run through `bunx`, so the published CLI is
8+
* fetched and executed without ever going through a pnpm install gate (which
9+
* would interactively prompt to approve esbuild's build script and hang CI).
10+
*
11+
* No npm dependencies: only Node/Bun built-ins. Bun runs this .ts file directly,
12+
* so there is no build step.
13+
*
14+
* Secrets (the API key) are passed to the child via the environment only, never
15+
* on argv, and are never printed.
16+
*/
17+
import { spawnSync } from "node:child_process";
18+
19+
function env(name: string): string | undefined {
20+
const v = process.env[name];
21+
if (v === undefined) return undefined;
22+
const trimmed = v.trim();
23+
return trimmed.length > 0 ? trimmed : undefined;
24+
}
25+
26+
/** Minimal POSIX-ish tokenizer for the generic action's raw `args` input. No eval. */
27+
function tokenizeArgs(input: string): string[] {
28+
const tokens: string[] = [];
29+
let cur = "";
30+
let inSingle = false;
31+
let inDouble = false;
32+
let sawToken = false;
33+
for (let i = 0; i < input.length; i++) {
34+
const ch = input[i];
35+
if (inSingle) {
36+
if (ch === "'") inSingle = false;
37+
else cur += ch;
38+
continue;
39+
}
40+
if (inDouble) {
41+
if (ch === '"') inDouble = false;
42+
else if (ch === "\\" && (input[i + 1] === '"' || input[i + 1] === "\\")) {
43+
cur += input[++i];
44+
} else cur += ch;
45+
continue;
46+
}
47+
if (ch === "'") { inSingle = true; sawToken = true; continue; }
48+
if (ch === '"') { inDouble = true; sawToken = true; continue; }
49+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
50+
if (sawToken) { tokens.push(cur); cur = ""; sawToken = false; }
51+
continue;
52+
}
53+
cur += ch;
54+
sawToken = true;
55+
}
56+
if (inSingle || inDouble) {
57+
throw new Error("AWU_ARGS: unbalanced quotes in args input");
58+
}
59+
if (sawToken) tokens.push(cur);
60+
return tokens;
61+
}
62+
63+
function pushFlag(argv: string[], flag: string, value: string | undefined) {
64+
if (value !== undefined) argv.push(flag, value);
65+
}
66+
67+
function buildArgv(): string[] {
68+
const command = env("AWU_COMMAND");
69+
if (!command) throw new Error("AWU_COMMAND is required");
70+
71+
const argv: string[] = [];
72+
73+
switch (command) {
74+
case "deploy-functions": {
75+
argv.push("--deployFunctions");
76+
pushFlag(argv, "--functionIds", env("AWU_FUNCTION_IDS"));
77+
pushFlag(argv, "--functionPath", env("AWU_FUNCTION_PATH"));
78+
pushFlag(argv, "--buildConcurrency", env("AWU_BUILD_CONCURRENCY"));
79+
pushFlag(argv, "--config", env("AWU_CONFIG"));
80+
// Single-function overrides (CLI rejects these unless exactly one fn is targeted).
81+
pushFlag(argv, "--functionName", env("AWU_FN_NAME"));
82+
pushFlag(argv, "--functionRuntime", env("AWU_FN_RUNTIME"));
83+
pushFlag(argv, "--functionEntrypoint", env("AWU_FN_ENTRYPOINT"));
84+
pushFlag(argv, "--functionCommands", env("AWU_FN_COMMANDS"));
85+
pushFlag(argv, "--functionSchedule", env("AWU_FN_SCHEDULE"));
86+
pushFlag(argv, "--functionTimeout", env("AWU_FN_TIMEOUT"));
87+
pushFlag(argv, "--functionScopes", env("AWU_FN_SCOPES"));
88+
pushFlag(argv, "--functionEvents", env("AWU_FN_EVENTS"));
89+
pushFlag(argv, "--functionExecute", env("AWU_FN_EXECUTE"));
90+
// Boolean overrides: pass as --flag=value so yargs records the explicit value.
91+
const enabled = env("AWU_FN_ENABLED");
92+
if (enabled !== undefined) argv.push(`--functionEnabled=${enabled}`);
93+
const logging = env("AWU_FN_LOGGING");
94+
if (logging !== undefined) argv.push(`--functionLogging=${logging}`);
95+
break;
96+
}
97+
case "push": {
98+
argv.push("--push");
99+
pushFlag(argv, "--config", env("AWU_CONFIG"));
100+
break;
101+
}
102+
case "raw": {
103+
const raw = env("AWU_ARGS");
104+
if (!raw) throw new Error("AWU_ARGS is required for the generic action");
105+
argv.push(...tokenizeArgs(raw));
106+
break;
107+
}
108+
default:
109+
throw new Error(`Unknown AWU_COMMAND: ${command}`);
110+
}
111+
112+
return argv;
113+
}
114+
115+
function main(): number {
116+
const argv = buildArgv();
117+
const cliVersion = env("AWU_CLI_VERSION") ?? "latest";
118+
const workingDir = env("AWU_WORKING_DIR") ?? ".";
119+
120+
const childEnv: NodeJS.ProcessEnv = { ...process.env };
121+
// The CLI reads these (resolveCliCredentials). Passing via env keeps the API
122+
// key off the command line and out of any logged argv.
123+
const endpoint = env("AWU_ENDPOINT");
124+
const projectId = env("AWU_PROJECT_ID");
125+
const apiKey = env("AWU_API_KEY");
126+
if (endpoint) childEnv.APPWRITE_ENDPOINT = endpoint;
127+
if (projectId) childEnv.APPWRITE_PROJECT_ID = projectId;
128+
if (apiKey) childEnv.APPWRITE_API_KEY = apiKey;
129+
130+
const bunxArgs = [
131+
"--package=appwrite-utils-cli@" + cliVersion,
132+
"appwrite-migrate",
133+
...argv,
134+
];
135+
136+
// argv contains no secrets (creds go via env), so this is safe to print.
137+
console.log(`> bunx ${bunxArgs.join(" ")} (cwd: ${workingDir})`);
138+
139+
const result = spawnSync("bunx", bunxArgs, {
140+
cwd: workingDir,
141+
env: childEnv,
142+
stdio: "inherit",
143+
});
144+
145+
if (result.error) {
146+
console.error(`Failed to run appwrite-migrate: ${result.error.message}`);
147+
return 1;
148+
}
149+
if (typeof result.status === "number") return result.status;
150+
// Terminated by signal.
151+
return 1;
152+
}
153+
154+
process.exit(main());
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: "Appwrite Utils — Run appwrite-migrate"
2+
description: "Run appwrite-utils-cli (appwrite-migrate) with arbitrary arguments, installed through bunx with no pnpm build prompt. Escape hatch for sync/import/generate/backup/regen and any other command."
3+
author: "Zach Handley"
4+
branding:
5+
icon: "terminal"
6+
color: "purple"
7+
8+
inputs:
9+
args:
10+
description: "Raw arguments passed to appwrite-migrate (e.g. '--sync' or '--import'). Supports single/double quotes; no shell evaluation."
11+
required: true
12+
endpoint:
13+
description: "Appwrite endpoint (e.g. https://cloud.appwrite.io/v1)."
14+
required: false
15+
default: ""
16+
project-id:
17+
description: "Appwrite project ID."
18+
required: false
19+
default: ""
20+
api-key:
21+
description: "Appwrite API key. Pass via secrets; it is forwarded to the CLI through the environment, never on the command line."
22+
required: false
23+
default: ""
24+
working-directory:
25+
description: "Directory to run the CLI from."
26+
required: false
27+
default: "."
28+
cli-version:
29+
description: "Version of appwrite-utils-cli to run via bunx (npm dist-tag or exact version)."
30+
required: false
31+
default: "latest"
32+
33+
runs:
34+
using: "composite"
35+
steps:
36+
- name: Setup Bun
37+
uses: oven-sh/setup-bun@v2
38+
39+
- name: Run appwrite-migrate
40+
shell: bash
41+
run: bun "${{ github.action_path }}/../_shared/run.ts"
42+
env:
43+
AWU_COMMAND: "raw"
44+
AWU_ARGS: ${{ inputs.args }}
45+
AWU_CLI_VERSION: ${{ inputs.cli-version }}
46+
AWU_WORKING_DIR: ${{ inputs.working-directory }}
47+
AWU_ENDPOINT: ${{ inputs.endpoint }}
48+
AWU_PROJECT_ID: ${{ inputs.project-id }}
49+
AWU_API_KEY: ${{ inputs.api-key }}

0 commit comments

Comments
 (0)