@yunuseon/kayra 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kayra-lab
|
|
3
|
+
description: Run kayra's upgrade lab (test/lab/upgrade.sh) - a throwaway Multipass server brought up at the last release, upgraded to the kayra checkout, and taken through every deploy path - and read its result. Use this before merging any change to kayra's platform (ansible/, compose/, scripts/, cli/src/deployer/, terraform/), after a release to prove the full path from it, whenever the user asks to "test this on a VM", "run the lab", "does the upgrade work", or when a bug was found on a real server and needs a reproduction.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The upgrade lab
|
|
7
|
+
|
|
8
|
+
What broke on kayra's real servers broke on **upgrades**, never on the fresh
|
|
9
|
+
VMs it used to be tested on: Authelia kept rules it had never loaded, an app
|
|
10
|
+
was left unregistered, every image was rebuilt on every run. The lab
|
|
11
|
+
reproduces the upgrade path end to end:
|
|
12
|
+
|
|
13
|
+
1. A temp fleet pinned to the last release (or `--from <version>`) brings a
|
|
14
|
+
Multipass server up and deploys a fixture app (`test/lab/app`) by a
|
|
15
|
+
signed webhook.
|
|
16
|
+
2. The fleet's kayra becomes this checkout; `kayra deploy` (which ends with
|
|
17
|
+
`kayra verify`).
|
|
18
|
+
3. A new commit by webhook; the same commit again must be `CACHED`.
|
|
19
|
+
4. A commit the compose policy refuses: the run fails naming the bind mount,
|
|
20
|
+
and the old release keeps serving.
|
|
21
|
+
5. `kayra app rollback` to the first release, the registry GC, `kayra verify`.
|
|
22
|
+
|
|
23
|
+
## Before you start
|
|
24
|
+
|
|
25
|
+
- Run it from a kayra checkout, or a worktree of one, that holds the change
|
|
26
|
+
under test. The lab links the fleet to **that directory's working tree**,
|
|
27
|
+
uncommitted changes included: say which commit and which changes it tests.
|
|
28
|
+
- `multipass list` shows no `kayra-lab` VM. A kept one from an earlier
|
|
29
|
+
failure is torn down first (its command was printed at the end of that run).
|
|
30
|
+
- Multipass, mkcert, OpenTofu, Ansible, Node 24 and python3 on the Mac.
|
|
31
|
+
|
|
32
|
+
## Running it
|
|
33
|
+
|
|
34
|
+
About 15 minutes. Run it in the background with its output in a log, and
|
|
35
|
+
follow the phases rather than waiting blind:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bash test/lab/upgrade.sh [--from <version>] [--keep] > <log> 2>&1; echo "lab exit $?" >> <log>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Watch for lines starting `── ` (a phase), `✓` and `✗`, `kept for`, and
|
|
42
|
+
`lab exit`. Keep one watcher per log: two watchers on the same file report
|
|
43
|
+
every event twice. A release older than the one that allowed `http://` app
|
|
44
|
+
repos on multipass prints "cannot clone over http://: the app arrives with
|
|
45
|
+
the upgrade". The release then sets up only the platform, which is expected
|
|
46
|
+
for that range.
|
|
47
|
+
|
|
48
|
+
## When it fails
|
|
49
|
+
|
|
50
|
+
A failure keeps everything, the VM (`kayra-lab`), the temp fleet and the log,
|
|
51
|
+
and prints where. Look before you change anything:
|
|
52
|
+
|
|
53
|
+
- The lab's own log: `grep -nE 'fatal:|FAILED|✗|kayra:|rror' <lab log>`.
|
|
54
|
+
- What the server did: from the kept fleet directory,
|
|
55
|
+
`node node_modules/@yunuseon/kayra/bin/kayra.js app runs lab sample` and
|
|
56
|
+
`… app logs lab sample`.
|
|
57
|
+
- Then decide: **a kayra bug or a lab bug?** Most first-run failures were the
|
|
58
|
+
lab's. Each was fixed in the lab, and twice the fix showed something kayra
|
|
59
|
+
should handle:
|
|
60
|
+
- git's dumb HTTP cannot serve the shallow clones kayra makes, so the lab
|
|
61
|
+
uses smart HTTP (`test/lab/git-http.py`);
|
|
62
|
+
- a repo URL without `owner/name` matches no forge, so its webhooks were
|
|
63
|
+
refused;
|
|
64
|
+
- a bind mount appended to the end of the compose file landed under
|
|
65
|
+
`networks:`, so compose rejected the file before the policy saw it;
|
|
66
|
+
- `verify` did not yet understand an app registered but never deployed:
|
|
67
|
+
that was a gap in kayra, fixed there, with a case added.
|
|
68
|
+
- Tear the kept VM down with the printed command before the next run.
|
|
69
|
+
|
|
70
|
+
## After a pass
|
|
71
|
+
|
|
72
|
+
The VM and the temp fleet are destroyed unless `--keep`. Report the five
|
|
73
|
+
phases and the final `kayra verify`, and what the run covered. A bug found on
|
|
74
|
+
a real server, which the lab did not catch, gets a step here: the lab is
|
|
75
|
+
only as good as the history it encodes.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kayra-release
|
|
3
|
+
description: Cut a release of kayra (the npm package @yunuseon/kayra) - version bump PR, checks, merge, tag vX.Y.Z, the release workflow publishing to npmjs.org, and checking the published package. Use this whenever kayra changes are merged and need to reach fleets, when the user says "release", "cut a release", "tag it", "publish kayra", "ship 0.x", or asks how to get merged kayra work onto servers - a fleet only gets kayra changes through a release.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Releasing kayra
|
|
7
|
+
|
|
8
|
+
A fleet runs the kayra version its `package.json` pins, from npmjs.org, so
|
|
9
|
+
merged work reaches no server until it is released. A release is a version
|
|
10
|
+
bump on `main`, a tag `v<version>` on the bump's merge commit, and
|
|
11
|
+
`.github/workflows/release.yml`, which publishes that tag to npm.
|
|
12
|
+
|
|
13
|
+
## Before anything: don't touch the user's checkout
|
|
14
|
+
|
|
15
|
+
The user works in the same checkouts in parallel: their own branch,
|
|
16
|
+
uncommitted changes, sometimes mid-task. Do all of this in a **separate git
|
|
17
|
+
worktree off `origin/main`**, and run every step with absolute paths inside
|
|
18
|
+
it. Write the steps as one script under `set -euo pipefail` with one command
|
|
19
|
+
per line. A guard such as `[ -z "$(git status --short)" ] && …` protects only
|
|
20
|
+
what is inside its `&&` chain: anything after a `;` runs regardless, and that
|
|
21
|
+
is how writes once landed in the user's working tree. If a step needs the
|
|
22
|
+
user's checkout (it rarely does here), stop and ask.
|
|
23
|
+
|
|
24
|
+
## 1. What goes into this release
|
|
25
|
+
|
|
26
|
+
- `git log --oneline v<last>..origin/main` and the merged PRs since the last tag.
|
|
27
|
+
- Check that every PR you count is really on `main`: a PR stacked on another
|
|
28
|
+
branch merges into **that** branch (`gh pr view <n> --json baseRefName`).
|
|
29
|
+
It happened once: the PR was merged, its work never reached `main`.
|
|
30
|
+
- **Stop when there is nothing to release.** If the log since the last tag is
|
|
31
|
+
empty, or a PR you were told is merged is still open (`gh pr view <n> --json
|
|
32
|
+
state`), the release would be empty: stop and say so, even when asked to
|
|
33
|
+
release. Make the script fail here (`test -n "$(git log …)"`, `test "$state"
|
|
34
|
+
= MERGED`), not print and go on: once a run printed `#41 OPEN` and went on to
|
|
35
|
+
open an empty release PR.
|
|
36
|
+
- Pick the version (0.x):
|
|
37
|
+
- **patch** for fixes;
|
|
38
|
+
- **minor** for a new command, a new setting, a behaviour change (for example
|
|
39
|
+
`deploy` newly ending with `verify`), and anything breaking for fleets.
|
|
40
|
+
- A breaking change lists exactly what a fleet must change, in the release
|
|
41
|
+
commit and the PR. Fleets read it when they bump.
|
|
42
|
+
|
|
43
|
+
## 2. The bump PR
|
|
44
|
+
|
|
45
|
+
In the worktree:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git -C "$W" switch -c release-<version>
|
|
49
|
+
npm --prefix "$W" version <version> --no-git-tag-version # package.json + package-lock.json only
|
|
50
|
+
git -C "$W" add package.json package-lock.json
|
|
51
|
+
git -C "$W" commit -s -F <message file> # DCO sign-off; the message summarises the PRs
|
|
52
|
+
git -C "$W" push -u origin release-<version>
|
|
53
|
+
gh pr create --repo yunuseon/kayra --base main --head release-<version> --title "<version>" --body-file <file>
|
|
54
|
+
gh pr checks release-<version> --repo yunuseon/kayra --watch --interval 20
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Write commit messages and PR bodies to a file first (quoted heredoc):
|
|
58
|
+
backticks inside `-m "…"` or a double-quoted `--body` are executed by the
|
|
59
|
+
shell.
|
|
60
|
+
|
|
61
|
+
## 3. Merge and tag
|
|
62
|
+
|
|
63
|
+
`main` is protected: it changes only through a PR with green `checks`, for
|
|
64
|
+
admins too. Merging and pushing a tag both ask the operator (kayra's guard);
|
|
65
|
+
that is expected, say what you are doing. In auto mode the guard refuses
|
|
66
|
+
them instead: stop, give the operator the exact command, and continue once
|
|
67
|
+
they ran it (or switched the session to Manual and said so).
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
gh pr merge <n> --repo yunuseon/kayra --merge --delete-branch
|
|
71
|
+
merge="$(gh pr view <n> --repo yunuseon/kayra --json mergeCommit --jq .mergeCommit.oid)"
|
|
72
|
+
git -C "$W" fetch -q origin && git -C "$W" checkout -q "$merge"
|
|
73
|
+
grep '"version"' "$W/package.json" # must be the new version
|
|
74
|
+
git -C "$W" tag v<version> # lightweight, on the merge commit, like every earlier tag
|
|
75
|
+
git -C "$W" push origin v<version>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Release tags are protected (a ruleset): once pushed, a tag cannot be moved or
|
|
79
|
+
deleted. A wrong tag is fixed by the next patch release, not by retagging.
|
|
80
|
+
So check the commit before you tag it.
|
|
81
|
+
|
|
82
|
+
## 4. Watch it publish
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
run="$(gh run list --repo yunuseon/kayra --workflow release.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
|
86
|
+
gh run watch "$run" --repo yunuseon/kayra --exit-status
|
|
87
|
+
npm view @yunuseon/kayra@<version> version # may lag a minute after the run: retry for ~2 minutes
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then look inside the published package for what this release added, not
|
|
91
|
+
just at its version. The fleets get the package, not the repo, and only the
|
|
92
|
+
paths in `package.json`'s `files` ship:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
d="$(mktemp -d)" && (cd "$d" && npm pack @yunuseon/kayra@<version> --silent && tar -tzf *.tgz | grep -E '<new files>')
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Test-only paths (`test/`, `ci/`) are meant to stay out.
|
|
99
|
+
|
|
100
|
+
## 5. Afterwards
|
|
101
|
+
|
|
102
|
+
- Remove the worktree and the local branch.
|
|
103
|
+
- The release reaches servers through each fleet: that is the **kayra-rollout**
|
|
104
|
+
skill. Say so, and say whether servers need a deploy at all (a release that
|
|
105
|
+
only changes the CLI or the agent tooling changes nothing on servers).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kayra-rollout
|
|
3
|
+
description: Bring a kayra fleet's servers to a new kayra release - the version bump PR in each fleet (in a separate git worktree), its checks, the merge, then kayra deploy server by server in a safe order with kayra verify after each. Use this whenever a kayra release exists that the servers don't run yet, when the user says "roll it out", "bump the fleets", "deploy the new kayra", "update the servers", "Renovate opened a kayra bump", or after the kayra-release skill finishes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rolling kayra out to the fleets
|
|
7
|
+
|
|
8
|
+
A fleet pins kayra in its `package.json`; its servers run what that pinned
|
|
9
|
+
kayra copies to them. Rolling out means: bump the pin in the fleet, merge,
|
|
10
|
+
then deploy each server, least important first, checking each before the
|
|
11
|
+
next.
|
|
12
|
+
|
|
13
|
+
## The rule that matters most: separate worktrees
|
|
14
|
+
|
|
15
|
+
The user works in the same checkouts in parallel: their own branch and
|
|
16
|
+
uncommitted changes. Make every change in a **separate git worktree off
|
|
17
|
+
`origin/main`**, with absolute paths, as one script under
|
|
18
|
+
`set -euo pipefail`, one command per line. Never let a step that writes run
|
|
19
|
+
after `;` behind a guard: `[ -z "$(git status --short)" ] && git switch -c x;
|
|
20
|
+
npm install …` ran the `npm install` in the user's tree. Record the user's
|
|
21
|
+
`git status --short` before and check it is unchanged at the end.
|
|
22
|
+
|
|
23
|
+
Deploying is different: `require-main.sh` refuses a real server unless **the
|
|
24
|
+
fleet's checkout** is a clean `main` equal to `origin/main`, with the pinned
|
|
25
|
+
kayra installed (not a link). If the user's checkout isn't (their branch,
|
|
26
|
+
their changes), don't switch it: tell them, and deploy when they say it is
|
|
27
|
+
on `main`.
|
|
28
|
+
|
|
29
|
+
## 1. Which fleets, which servers, which order
|
|
30
|
+
|
|
31
|
+
- The fleets are the repositories that pin `@yunuseon/kayra`. Their own
|
|
32
|
+
CLAUDE.md or README names them, how their PRs work (GitHub `gh`, Forgejo
|
|
33
|
+
`tea`), and the rollout order. Read that and follow it.
|
|
34
|
+
- Without an order given: multipass and test servers first, then home
|
|
35
|
+
servers, then real ones (hetzner), the least important first. Platform
|
|
36
|
+
servers get `kayra deploy`; edge and NAS servers get `kayra setup`.
|
|
37
|
+
- Read the release notes between the fleet's version and the new one. A
|
|
38
|
+
breaking release says exactly what a fleet must change (settings renamed or
|
|
39
|
+
removed, new secrets): those changes go into the same bump PR.
|
|
40
|
+
|
|
41
|
+
## 2. The bump PR, per fleet
|
|
42
|
+
|
|
43
|
+
In a worktree of the fleet:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install --prefix "$W" --silent -D -E @yunuseon/kayra@<version>
|
|
47
|
+
# …the fleet changes the release asks for (vault edits: decrypt in memory, never print a value)
|
|
48
|
+
cd "$W" && kayra validate
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run the fleet's checks the way its CI does, against **tracked files only**
|
|
52
|
+
(untracked local folders such as a lab server must not count): copy
|
|
53
|
+
`git ls-files` plus `node_modules` into a temp context and
|
|
54
|
+
`docker build -f <kayra checkout>/ci/checks/Dockerfile --build-context fleet=<context> --build-arg CHECKS="--fleet /fleet" <kayra checkout>`,
|
|
55
|
+
with the kayra checkout at the released tag.
|
|
56
|
+
|
|
57
|
+
Commit (the message names the release and anything the fleet changed),
|
|
58
|
+
push, open the PR the fleet's way, wait for its checks (GitHub:
|
|
59
|
+
`gh pr checks --watch`; Forgejo: `tea api /repos/<owner>/<repo>/commits/<sha>/status`
|
|
60
|
+
until `success`), then merge. Merges ask the operator (kayra's guard); that
|
|
61
|
+
is expected. In auto mode the guard refuses them, and every non-multipass
|
|
62
|
+
deploy below, instead: stop, give the operator the exact command, continue
|
|
63
|
+
once they ran it. Remove the worktree and the local branch.
|
|
64
|
+
|
|
65
|
+
## 3. Deploy, server by server
|
|
66
|
+
|
|
67
|
+
From the fleet's own checkout once it is on the merged `main` (`git pull`,
|
|
68
|
+
then `npm ci`, so `node_modules` is the released version):
|
|
69
|
+
|
|
70
|
+
- `kayra deploy <platform server>`, **not** `kayra setup`: a new version may
|
|
71
|
+
change the app role, which only `apps` runs. `deploy` ends with
|
|
72
|
+
`kayra verify` and fails if the server is broken. Read every `✗`.
|
|
73
|
+
- `kayra setup <edge or nas server>`. A NAS's SSH may be on only on demand:
|
|
74
|
+
if it refuses the connection, ask the user to switch it on. Don't retry in
|
|
75
|
+
a loop.
|
|
76
|
+
- One server at a time, in order. Stop at the first failure, find the cause,
|
|
77
|
+
and fix it in kayra if it is kayra's, before touching the next server.
|
|
78
|
+
|
|
79
|
+
Things that have bitten before:
|
|
80
|
+
- **A new platform hostname needs DNS before the deploy.** Traefik asks
|
|
81
|
+
Let's Encrypt once; a name without DNS keeps Traefik's default certificate
|
|
82
|
+
until Traefik restarts. `verify` reports it as `certificate by TRAEFIK
|
|
83
|
+
DEFAULT CERT`.
|
|
84
|
+
- **A Docker upgrade by the docker role restarts every container.** That's
|
|
85
|
+
normal; check the server's public routes afterwards.
|
|
86
|
+
- **Changed platform config on an upgraded server:** `verify` catches what
|
|
87
|
+
did not reload (a 403 from Authelia meant rules it had never loaded).
|
|
88
|
+
|
|
89
|
+
## 4. Report
|
|
90
|
+
|
|
91
|
+
Per server: the version it runs now, the deploy's recap (nothing failed?),
|
|
92
|
+
the `kayra verify` result, and anything left for the user (a DNS apply, a
|
|
93
|
+
switch in a vendor UI, a manual cleanup). Then whether the user's checkouts
|
|
94
|
+
are as they left them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yunuseon/kayra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "A self-hosted app platform and the CLI that runs it: Traefik, Authelia, a registry, rootless BuildKit and a small deployer per server, set up with OpenTofu and Ansible.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Yunus Öztürk",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"compose/",
|
|
23
23
|
"terraform/",
|
|
24
24
|
"scripts/",
|
|
25
|
+
"agent/",
|
|
25
26
|
"cloud-init.yml.tpl",
|
|
26
27
|
"hosts.conf",
|
|
27
28
|
"schema/"
|
package/scripts/agent/guard.sh
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
# command that changes something real waits for the operator to approve it,
|
|
6
6
|
# in that moment.
|
|
7
7
|
#
|
|
8
|
-
# Reads the hook's JSON on stdin; prints a permissionDecision
|
|
9
|
-
#
|
|
8
|
+
# Reads the hook's JSON on stdin; prints a permissionDecision with the reason
|
|
9
|
+
# for the commands below: "ask" in a mode that shows the operator a prompt
|
|
10
|
+
# (default, acceptEdits, plan), "deny" in one that approves by itself (auto,
|
|
11
|
+
# dontAsk, bypassPermissions) or an unknown one, where an "ask" would let the
|
|
12
|
+
# command through unseen. Denied, the agent stops and says so; the operator
|
|
13
|
+
# runs the command, or switches the session to Manual for it.
|
|
10
14
|
# - kayra commands that change a server (up, provision, setup, deploy,
|
|
11
15
|
# apps, adopt, down, app purge|deploy|rollback, job) unless --dry-run or
|
|
12
16
|
# the server is a multipass one (its server.yml under the project)
|
|
@@ -25,13 +29,19 @@
|
|
|
25
29
|
# =============================================================================
|
|
26
30
|
set -uo pipefail
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
input="$(cat)"
|
|
33
|
+
command="$(jq -r '.tool_input.command // empty' <<< "$input" 2>/dev/null)"
|
|
29
34
|
[ -n "$command" ] || exit 0
|
|
35
|
+
mode="$(jq -r '.permission_mode // empty' <<< "$input" 2>/dev/null)"
|
|
30
36
|
project="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
31
37
|
|
|
32
38
|
ask() {
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
case "$mode" in
|
|
40
|
+
default|acceptEdits|plan) decision=ask reason="guard: $1" ;;
|
|
41
|
+
*) decision=deny reason="guard: $1. This session approves by itself (${mode:-no permission mode}), so the guard refuses instead of asking: tell the operator, who runs it or switches the session to Manual for it." ;;
|
|
42
|
+
esac
|
|
43
|
+
jq -n --arg decision "$decision" --arg reason "$reason" \
|
|
44
|
+
'{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: $decision, permissionDecisionReason: $reason}}'
|
|
35
45
|
exit 0
|
|
36
46
|
}
|
|
37
47
|
has() { grep -qE -- "$1" <<< "$command"; }
|