Skip to content

Commit 6e154b5

Browse files
Add pi-node2 image deployment runbook
1 parent 2020c08 commit 6e154b5

11 files changed

Lines changed: 807 additions & 6 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: candidate-image
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
image_tag:
7+
description: "Optional candidate tag. Defaults to codex-cli-provider-dev-<branch>-<short-sha>."
8+
required: false
9+
type: string
10+
platforms:
11+
description: "Target image platform(s). Use linux/arm64 for pi-node2 validation."
12+
required: true
13+
default: linux/arm64
14+
type: choice
15+
options:
16+
- linux/arm64
17+
- linux/amd64
18+
- linux/amd64,linux/arm64
19+
20+
permissions:
21+
contents: read
22+
packages: write
23+
24+
concurrency:
25+
group: candidate-image-${{ github.ref }}
26+
cancel-in-progress: true
27+
28+
jobs:
29+
publish-candidate-image:
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 45
32+
33+
steps:
34+
- name: Checkout
35+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
36+
37+
- name: Set up Python
38+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
39+
with:
40+
python-version: "3.12"
41+
cache: pip
42+
43+
- name: Install dependencies
44+
run: pip install --requirement requirements.txt
45+
46+
- name: Unit tests
47+
run: python -m pytest -q
48+
49+
- name: Syntax checks
50+
run: python -m compileall -q src scripts tests
51+
52+
- name: Prepare throwaway local config
53+
run: |
54+
cp .env.example .env
55+
mkdir -p data/secrets data/codex-home data/codex-work
56+
python - <<'PY'
57+
import pathlib
58+
import secrets
59+
60+
pathlib.Path("data/secrets/proxy_api_key").write_text(secrets.token_urlsafe(48) + "\n", encoding="utf-8")
61+
PY
62+
chmod 600 .env
63+
chmod 644 data/secrets/proxy_api_key
64+
chmod 700 data/secrets data/codex-home data/codex-work
65+
66+
- name: Repo hygiene checks
67+
run: python scripts/check_repo_hygiene.py
68+
69+
- name: Compose security checks
70+
run: python scripts/check_compose_security.py
71+
72+
- name: Image Compose security checks
73+
env:
74+
COMPOSE_FILE: docker-compose.image.yml
75+
CODEX_CLI_PROVIDER_IMAGE: ghcr.io/${{ github.repository }}:codex-cli-provider-dev-compose-check
76+
run: python scripts/check_compose_security.py
77+
78+
- name: Resolve candidate tag
79+
id: candidate
80+
env:
81+
REQUESTED_IMAGE_TAG: ${{ inputs.image_tag }}
82+
run: >
83+
python scripts/image_tags.py candidate
84+
--ref-name "$GITHUB_REF_NAME"
85+
--sha "$GITHUB_SHA"
86+
--requested "$REQUESTED_IMAGE_TAG"
87+
--github-output
88+
89+
- name: Set up QEMU
90+
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
91+
92+
- name: Set up Docker Buildx
93+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
94+
95+
- name: Log in to GHCR
96+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
97+
with:
98+
registry: ghcr.io
99+
username: ${{ github.actor }}
100+
password: ${{ secrets.GITHUB_TOKEN }}
101+
102+
- name: Build and publish candidate image
103+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
104+
with:
105+
context: .
106+
file: Dockerfile
107+
platforms: ${{ inputs.platforms }}
108+
push: true
109+
tags: ghcr.io/${{ github.repository }}:${{ steps.candidate.outputs.tag }}
110+
labels: |
111+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
112+
org.opencontainers.image.revision=${{ github.sha }}
113+
org.opencontainers.image.version=${{ steps.candidate.outputs.tag }}
114+
115+
- name: Inspect candidate image
116+
run: docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ steps.candidate.outputs.tag }}
117+
118+
- name: Print pi-node2 pull command
119+
run: |
120+
echo "Candidate image:"
121+
echo "ghcr.io/${{ github.repository }}:${{ steps.candidate.outputs.tag }}"
122+
echo
123+
echo "pi-node2 command:"
124+
echo "CODEX_CLI_PROVIDER_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.candidate.outputs.tag }} docker compose -f docker-compose.image.yml up -d"

.github/workflows/ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
run: python -m pytest -q
3838

3939
- name: Syntax checks
40-
run: python -m py_compile src/server.py scripts/check_compose_security.py scripts/check_repo_hygiene.py tests/test_server.py
40+
run: python -m compileall -q src scripts tests
4141

4242
- name: Prepare throwaway local config
4343
run: |
@@ -62,5 +62,11 @@ jobs:
6262
- name: Compose security checks
6363
run: python scripts/check_compose_security.py
6464

65+
- name: Image Compose security checks
66+
env:
67+
COMPOSE_FILE: docker-compose.image.yml
68+
CODEX_CLI_PROVIDER_IMAGE: registry.example.com/your-org/codex-cli-provider:codex-cli-provider-0.1.2
69+
run: python scripts/check_compose_security.py
70+
6571
- name: Docker build
6672
run: docker compose build
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: deploy-pi-node2
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
image_tag:
7+
description: "Candidate or release image tag to deploy, for example codex-cli-provider-dev-branch-abcdef123456."
8+
required: true
9+
type: string
10+
chat_smoke:
11+
description: "Run one live Codex-backed chat completion after deploy."
12+
required: true
13+
default: false
14+
type: boolean
15+
16+
permissions:
17+
contents: read
18+
19+
concurrency:
20+
group: deploy-pi-node2
21+
cancel-in-progress: false
22+
23+
jobs:
24+
deploy:
25+
runs-on: [self-hosted, linux, arm64, pi-node2]
26+
environment: pi-node2
27+
timeout-minutes: 20
28+
29+
steps:
30+
- name: Update fixed deploy checkout
31+
env:
32+
DEPLOY_DIR: ${{ vars.PI_NODE2_DEPLOY_DIR || '/home/pi/projects/codex-cli-provider' }}
33+
run: |
34+
set -eu
35+
mkdir -p "$(dirname "$DEPLOY_DIR")"
36+
if [ ! -d "$DEPLOY_DIR/.git" ]; then
37+
git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$DEPLOY_DIR"
38+
fi
39+
cd "$DEPLOY_DIR"
40+
git fetch --prune origin "$GITHUB_REF"
41+
git checkout --force "$GITHUB_SHA"
42+
43+
- name: Deploy and smoke test
44+
env:
45+
DEPLOY_DIR: ${{ vars.PI_NODE2_DEPLOY_DIR || '/home/pi/projects/codex-cli-provider' }}
46+
IMAGE_REPOSITORY: ghcr.io/${{ github.repository }}
47+
IMAGE_TAG: ${{ inputs.image_tag }}
48+
CHAT_SMOKE: ${{ inputs.chat_smoke }}
49+
run: |
50+
set -eu
51+
chat_arg=""
52+
if [ "$CHAT_SMOKE" = "true" ]; then
53+
chat_arg="--chat-smoke"
54+
fi
55+
cd "$DEPLOY_DIR"
56+
python3 scripts/deploy_compose_image.py \
57+
--image-repository "$IMAGE_REPOSITORY" \
58+
--image-tag "$IMAGE_TAG" \
59+
$chat_arg

.github/workflows/release.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch:
55
inputs:
66
image_tag:
7-
description: "Image tag to publish, for example v0.1.0 or test"
7+
description: "Image tag to publish, for example codex-cli-provider-0.1.0"
88
required: true
99
type: string
1010
push:
@@ -28,6 +28,19 @@ jobs:
2828
- name: Checkout
2929
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
3030

31+
- name: Validate dispatch image tag
32+
if: github.event_name == 'workflow_dispatch'
33+
run: python scripts/image_tags.py validate --kind release "${{ inputs.image_tag }}"
34+
35+
- name: Validate git tag image tag
36+
if: github.event_name == 'push'
37+
run: |
38+
release_tag="codex-cli-provider-${GITHUB_REF_NAME#v}"
39+
python scripts/image_tags.py validate --kind release "$release_tag"
40+
41+
- name: Set up QEMU
42+
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
43+
3144
- name: Set up Docker Buildx
3245
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
3346

@@ -45,7 +58,7 @@ jobs:
4558
images: ghcr.io/${{ github.repository }}
4659
tags: |
4760
type=raw,value=${{ inputs.image_tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
48-
type=ref,event=tag
61+
type=semver,pattern=codex-cli-provider-{{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
4962
5063
- name: Build and publish image
5164
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ data/
1313
logs/
1414
tmp/
1515
temp/
16+
handoff.md
1617

1718
.DS_Store
1819
.idea/

README.md

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,107 @@ OpenAI API keys in Compose files.
300300
Codex authentication still lives only in the mounted `data/codex-home`
301301
directory and must be completed inside the running container.
302302

303+
## Pre-Merge pi-node2 Image Test
304+
305+
Use the `candidate-image` GitHub Actions workflow to publish a disposable image
306+
from a branch before merging it. The default target is `linux/arm64` for
307+
`pi-node2` validation, and the generated tag looks like:
308+
309+
```text
310+
codex-cli-provider-dev-branch-name-abcdef123456
311+
```
312+
313+
Run the workflow from the branch you want to test. Leave `image_tag` empty
314+
unless you need a stable candidate tag; custom candidate tags must start with
315+
`codex-cli-provider-dev-`. Do not use `latest`.
316+
317+
On `pi-node2`, keep using the image-only Compose file and the dedicated local
318+
`data/codex-home` auth mount:
319+
320+
```bash
321+
docker login ghcr.io
322+
export CODEX_CLI_PROVIDER_IMAGE=ghcr.io/subdepthtech/codex-cli-provider:codex-cli-provider-dev-branch-name-abcdef123456
323+
docker compose -f docker-compose.image.yml pull
324+
docker compose -f docker-compose.image.yml up -d
325+
python3 scripts/smoke_test_provider.py
326+
```
327+
328+
To verify one real Codex-backed request after the container is healthy and
329+
logged in, run:
330+
331+
```bash
332+
python3 scripts/smoke_test_provider.py --chat
333+
```
334+
335+
The non-chat smoke test checks `/healthz`, confirms `/v1/models` rejects
336+
unauthenticated callers, and confirms the authenticated model list includes
337+
`codex-cli-default`. The `--chat` check sends one live upstream request through
338+
the signed-in Codex CLI account.
339+
340+
After the `pi-node2` smoke test passes, merge the branch and publish the release
341+
image. The release workflow publishes versioned tags like
342+
`codex-cli-provider-0.1.2` from either a manual dispatch or a Git tag such as
343+
`v0.1.2`.
344+
345+
## Automated pi-node2 Deployment
346+
347+
The `deploy-pi-node2` GitHub Actions workflow deploys an already-published
348+
candidate or release image on a self-hosted runner installed on `pi-node2`.
349+
It intentionally does not use SSH keys or GitHub-hosted runner secrets.
350+
351+
One-time `pi-node2` setup:
352+
353+
```bash
354+
mkdir -p ~/projects
355+
git clone https://github.com/subdepthtech/codex-cli-provider.git ~/projects/codex-cli-provider
356+
cd ~/projects/codex-cli-provider
357+
cp .env.example .env
358+
mkdir -p data/codex-home data/codex-work data/secrets
359+
python3 - <<'PY'
360+
import pathlib, secrets
361+
path = pathlib.Path("data/secrets/proxy_api_key")
362+
path.write_text(secrets.token_urlsafe(48) + "\n")
363+
PY
364+
chmod 600 .env data/secrets/proxy_api_key
365+
chmod 700 data/codex-home data/codex-work data/secrets
366+
docker login ghcr.io
367+
```
368+
369+
The deploy smoke test expects the dedicated `data/codex-home` mount on
370+
`pi-node2` to already contain a valid Codex login. After starting the container
371+
with a candidate or release image for the first time, complete device login
372+
inside that container:
373+
374+
```bash
375+
docker exec -it codex-cli-provider \
376+
codex login --device-auth \
377+
-c forced_login_method='"chatgpt"' \
378+
-c cli_auth_credentials_store='"file"'
379+
```
380+
381+
Install the GitHub self-hosted runner on `pi-node2` with labels including
382+
`self-hosted`, `linux`, `arm64`, and `pi-node2`. In GitHub, create an
383+
environment named `pi-node2` and require manual approval before deployment.
384+
For a public repository, do not let untrusted pull requests run jobs on this
385+
runner.
386+
387+
Run the `deploy-pi-node2` workflow from a trusted branch, preferably `main`, and
388+
pass the exact image tag printed by `candidate-image`. The workflow uses
389+
`/home/pi/projects/codex-cli-provider` by default; set the repository or
390+
environment variable `PI_NODE2_DEPLOY_DIR` to override that path.
391+
392+
The deploy workflow updates the fixed checkout, validates the image tag, runs
393+
the image-only Compose security check, pulls the image, restarts the service,
394+
and runs `scripts/smoke_test_provider.py`. Enable `chat_smoke` to run one live
395+
Codex-backed request after restart.
396+
397+
## Host Reimage Runbook
398+
399+
For rebuilding the homelab `pi-node2` host, use
400+
[`docs/pi-node2-reimage.md`](docs/pi-node2-reimage.md). Keep host-specific
401+
values, backup locations, runner registration details, and credential recovery
402+
notes in the ignored top-level `handoff.md` file, not in tracked documentation.
403+
303404
## Verification
304405

305406
Run repository checks without live credentials:
@@ -316,9 +417,8 @@ PYTHONPATH=. .venv/bin/pytest -q
316417
Live checks require a running container and the dedicated Codex login:
317418

318419
```bash
319-
PROXY_API_KEY="$(cat data/secrets/proxy_api_key)"
320-
curl -f http://127.0.0.1:8320/healthz
321-
curl -f -H "Authorization: Bearer $PROXY_API_KEY" http://127.0.0.1:8320/v1/models
420+
python3 scripts/smoke_test_provider.py
421+
python3 scripts/smoke_test_provider.py --chat
322422
```
323423

324424
Do not print or inspect `data/codex-home/auth.json`.

0 commit comments

Comments
 (0)