Adds HOSTINGER_GITEA_RUNNER_ROADMAP.md — a single execution tracker that Codex on the Hostinger VM works through phase-by-phase, ticking checkboxes and recording commit hashes as it goes. Structure: - 6 phases (P0 Pre-flight → P5 First real release) + P6 review handoff - Each task: [ ] checkbox + Commit hash field + Status note - Detail steps live in the two companion docs (act_runner setup + publish workflow); the roadmap is the orchestrator - Final report section Codex fills in when P0-P5 are complete - Human review checklist (R1-R9) for verification after handoff - Operating notes: commit message format, when to ask, never-do list - Change log table Codex auto-appends to Critical invariant repeated at P3.6 and P5.4: cross-Gitea SHA1 comparison must match. If it doesn't, Codex stops — it's the load-bearing architectural guarantee that the dual-Gitea, no-sync- script model rests on. Also adds roadmap-pointer banners to the two companion docs (HOSTINGER_GITEA_ACT_RUNNER_SETUP.md, GITEA_PACKAGES_PUBLISH_WORKFLOW.md) so anyone landing there knows the master tracker exists.
16 KiB
@bytelyst/* Package Publish Workflow (Gitea Actions)
📋 Track progress in the master roadmap:
HOSTINGER_GITEA_RUNNER_ROADMAP.md— Phase 4 references this doc. Codex updates the roadmap checkboxes as each step here completes.
Delegation prompt for a coding agent. Implements the canonical
publish-packages.ymlworkflow that publishes@bytelyst/*packages to the Gitea npm registry on everyv*tag, and propagates it to all 20+ ByteLyst repos.
Companions:
HOSTINGER_GITEA_RUNNER_ROADMAP.md— execution tracker.HOSTINGER_GITEA_ACT_RUNNER_SETUP.md— the runner that executes this workflow.
1. Goal
When a v* tag is pushed to either Gitea instance:
- Corp Mac local Gitea (
http://localhost:3300) → corp Gitea Actions runner builds + publishes to corp Gitea. - Hostinger Gitea (
http://localhost:3300on the VM) → Hostinger Gitea Actions runner builds + publishes to Hostinger Gitea.
Because both runners build inside the same deterministic Docker image (node:20-bookworm pinned by digest) from the same git tag using the same lockfile, the resulting tarballs are byte-identical. SHA512 integrity hashes match → lockfiles portable across both registries.
The corp Mac never reaches Hostinger; the Hostinger VM never reaches corp Mac. The shared git tag IS the synchronization mechanism.
2. The workflow
Location: .gitea/workflows/publish-packages.yml in learning_ai_common_plat first; then propagated to every repo that ships @bytelyst/* packages.
# .gitea/workflows/publish-packages.yml
name: Publish @bytelyst/* packages
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
dry_run:
description: 'Build + pack but skip publish'
required: false
default: 'false'
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false # NEVER cancel a publish in flight
jobs:
publish:
runs-on: [ubuntu-latest, bytelyst]
container:
image: node:20-bookworm@sha256:PIN_THIS_DIGEST_FOR_DETERMINISM
options: --network host -v /home/gitea-runner/.gitea_npm_token:/run/secrets/gitea_npm_token:ro
steps:
- name: Checkout (full history needed for tag context)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Print build context
run: |
echo "Tag: ${{ github.ref_name }}"
echo "Commit: ${{ github.sha }}"
echo "Runner: $(hostname)"
echo "Image: $(cat /etc/os-release | grep ^PRETTY_NAME)"
node --version
npm --version
- name: Install pnpm 9 (pinned)
run: npm install -g pnpm@9.12.0
- name: Install workspace deps (frozen lockfile = determinism)
run: HUSKY=0 pnpm install --frozen-lockfile
- name: Build all packages
run: pnpm -r --filter "@bytelyst/*" run build
- name: Run tests
run: pnpm -r --filter "@bytelyst/*" test
- name: Configure registry .npmrc
run: |
TOKEN=$(cat /run/secrets/gitea_npm_token)
cat > /tmp/publish.npmrc <<NPMRC
@bytelyst:registry=http://localhost:3300/api/packages/bytelyst/npm/
//localhost:3300/api/packages/bytelyst/npm/:_authToken=$TOKEN
NPMRC
echo "Configured registry:"
sed 's|_authToken=.*|_authToken=***|' /tmp/publish.npmrc
- name: Publish changed packages (dry-run if input flag set)
env:
DRY_RUN: ${{ inputs.dry_run }}
run: |
PUBLISH_FLAGS="--no-git-checks --userconfig /tmp/publish.npmrc"
if [ "$DRY_RUN" = "true" ]; then
echo "DRY RUN — not publishing"
pnpm -r --filter "@bytelyst/*" publish $PUBLISH_FLAGS --dry-run
else
pnpm -r --filter "@bytelyst/*" publish $PUBLISH_FLAGS
fi
- name: Pack tarballs for tag asset upload
run: |
mkdir -p /tmp/tarballs
for pkg in packages/*/; do
if [ -f "$pkg/package.json" ] && grep -q '"name": "@bytelyst/' "$pkg/package.json"; then
(cd "$pkg" && pnpm pack --pack-destination /tmp/tarballs)
fi
done
ls -la /tmp/tarballs
- name: Compute SHA512 manifest (for cross-Gitea verification)
run: |
cd /tmp/tarballs
sha512sum *.tgz > manifest.sha512
cat manifest.sha512
- name: Upload tarballs + manifest as Gitea Release assets
if: startsWith(github.ref, 'refs/tags/v')
env:
GITEA_TOKEN: ${{ secrets.GITEA_NPM_TOKEN }}
run: |
TAG="${{ github.ref_name }}"
REPO="${{ github.repository }}" # e.g. bytelyst/learning_ai_common_plat
GITEA_URL="http://localhost:3300"
# Get release ID (create if doesn't exist)
RELEASE_ID=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/repos/$REPO/releases/tags/$TAG" \
| grep -oE '"id":[0-9]+' | head -1 | cut -d: -f2)
if [ -z "$RELEASE_ID" ]; then
RELEASE_ID=$(curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"@bytelyst/* packages published by Gitea Actions\"}" \
"$GITEA_URL/api/v1/repos/$REPO/releases" \
| grep -oE '"id":[0-9]+' | head -1 | cut -d: -f2)
fi
echo "Release ID: $RELEASE_ID"
# Upload each tarball + manifest
for f in /tmp/tarballs/*; do
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$f" \
"$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$(basename $f)" \
| head -200
echo
done
Key design choices in this workflow
| Decision | Reason |
|---|---|
node:20-bookworm@sha256:<digest> pinned |
Byte-identical builds across runners require identical OS/toolchain. Digest pin > floating tag. |
pnpm install --frozen-lockfile |
Lockfile is the dependency truth; CI never resolves fresh. |
pnpm@9.12.0 pinned |
Different pnpm minor versions can produce slightly different node_modules layouts; pinning avoids drift. |
--network host for the runner container |
Allows localhost:3300 (host's Gitea) to be reachable from inside the container. |
Token mounted as a read-only secret file (/run/secrets/...) |
Better than env var — not leaked to logs, not visible in process list. |
concurrency: cancel-in-progress: false |
NEVER cancel a publish mid-flight; would leave the registry in an inconsistent state. |
| Release assets in addition to registry publish | Gives a self-describing audit trail and a backup distribution channel. |
| SHA512 manifest as a release asset | Lets you verify the same tag built byte-identically on the other Gitea instance. |
Pin the Node image digest before merging
# Get current digest for node:20-bookworm
docker pull node:20-bookworm
docker inspect node:20-bookworm | grep -oE 'sha256:[a-f0-9]{64}' | head -1
# Update the workflow YAML with that digest, commit.
Re-pin every ~3 months or when a security patch is needed.
3. Propagation to all 20+ repos
Two strategies, choose one:
Option A — Reusable workflow (preferred, requires Gitea ≥ 1.22)
Define the publish workflow as workflow_call once in learning_ai_common_plat:
# learning_ai_common_plat/.gitea/workflows/_reusable-publish-packages.yml
name: Reusable — Publish @bytelyst/* packages
on:
workflow_call:
inputs:
packages_glob:
type: string
default: '@bytelyst/*'
secrets:
GITEA_NPM_TOKEN:
required: true
jobs:
publish:
# ... same body as above, parameterized by inputs.packages_glob
Then each consuming repo gets a 10-line stub:
# any-other-repo/.gitea/workflows/publish-packages.yml
name: Publish @bytelyst/* packages
on:
push:
tags: ['v*']
jobs:
call-reusable:
uses: bytelyst/learning_ai_common_plat/.gitea/workflows/_reusable-publish-packages.yml@main
secrets:
GITEA_NPM_TOKEN: ${{ secrets.GITEA_NPM_TOKEN }}
Update once in common-plat → all consumers automatically updated.
Option B — Sync script (follows your existing sync-npmrc.sh pattern)
If reusable workflows aren't available or you want explicit per-repo copies, use this script. Stored at learning_ai_common_plat/scripts/sync-publish-workflow.sh:
#!/usr/bin/env bash
# Propagates publish-packages.yml from common-plat to every repo that ships @bytelyst/* packages.
#
# Usage:
# bash scripts/sync-publish-workflow.sh # dry-run, show diffs
# bash scripts/sync-publish-workflow.sh --apply # actually copy + commit + push
set -euo pipefail
SOURCE=".gitea/workflows/publish-packages.yml"
WORKSPACE_ROOT=$(cd "$(dirname "$0")/.." && pwd)
SIBLINGS_ROOT=$(cd "$WORKSPACE_ROOT/.." && pwd)
# Repos that ship @bytelyst/* packages (add new ones here as the ecosystem grows)
TARGETS=(
"learning_ai_common_plat"
# Add others here when they start publishing their own scoped packages.
# Currently common-plat is the only publisher.
)
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1
for repo in "${TARGETS[@]}"; do
TARGET_DIR="$SIBLINGS_ROOT/$repo"
TARGET_FILE="$TARGET_DIR/.gitea/workflows/publish-packages.yml"
if [ ! -d "$TARGET_DIR" ]; then
echo "SKIP (not cloned locally): $repo"
continue
fi
if [ "$repo" = "learning_ai_common_plat" ]; then
echo "SKIP (source repo): $repo"
continue
fi
mkdir -p "$(dirname "$TARGET_FILE")"
if [ ! -f "$TARGET_FILE" ] || ! diff -q "$WORKSPACE_ROOT/$SOURCE" "$TARGET_FILE" > /dev/null; then
echo "=== $repo: publish-packages.yml differs from source ==="
diff "$WORKSPACE_ROOT/$SOURCE" "$TARGET_FILE" || true
if [ $APPLY -eq 1 ]; then
cp "$WORKSPACE_ROOT/$SOURCE" "$TARGET_FILE"
(cd "$TARGET_DIR" && git add .gitea/workflows/publish-packages.yml \
&& git commit -m "ci: sync publish-packages.yml from common-plat" \
&& git push origin main && git push gitea main)
echo " → applied + pushed"
fi
else
echo "OK: $repo (in sync)"
fi
done
Make executable: chmod +x scripts/sync-publish-workflow.sh. Run periodically (or on every common-plat change to the workflow).
4. Releasing a new package version (operator workflow)
# In the repo that owns the package(s) you're releasing:
cd ~/code/mygh/learning_ai_common_plat
# Bump version(s)
pnpm --filter @bytelyst/api-client version patch
# Or, for many packages at once, use changesets / pnpm -r
# Commit + tag + push to BOTH remotes
git add -A
git commit -m "chore: release @bytelyst/api-client v1.2.4"
git tag v1.2.4
git push origin main --tags # GitHub gets the tag (code record)
git push gitea main --tags # Local/Hostinger Gitea gets the tag (TRIGGERS CI)
# Watch the publish on both Giteas:
# - Corp: http://localhost:3300/bytelyst/learning_ai_common_plat/actions
# - Hostinger: http://gitea.bytelyst.com/bytelyst/learning_ai_common_plat/actions
# (only reachable from home network)
After both publish workflows complete, verify the byte-identical-tarball invariant:
HOST_SHA=$(curl -s "http://gitea.bytelyst.com/api/packages/bytelyst/npm/@bytelyst%2Fapi-client/1.2.4" \
| grep -oE '"shasum":"[^"]*"' | head -1 | cut -d'"' -f4)
CORP_SHA=$(curl -s -H "Authorization: token $(cat ~/.gitea_npm_token)" \
"http://localhost:3300/api/packages/bytelyst/npm/@bytelyst%2Fapi-client/1.2.4" \
| grep -oE '"shasum":"[^"]*"' | head -1 | cut -d'"' -f4)
[ "$HOST_SHA" = "$CORP_SHA" ] && echo "✅ Byte-identical across Giteas" \
|| echo "❌ DIVERGED — check Node/pnpm/lockfile versions"
5. End-to-end smoke run after first deployment
- Commit this workflow file to a branch in
learning_ai_common_plat. Don't merge yet. - From that branch, trigger via
workflow_dispatchwithdry_run: true. Confirms parsing + build + pack work without polluting the registry. - Merge to
main. - Bump a real package version (or use the throwaway
@bytelyst/_runner-e2e-testfrom theact_runnersetup doc). - Tag + push. Watch CI on both Giteas. Verify SHA match.
- Sanity-check from a consumer:
cd ~/code/mygh/learning_ai_clock pnpm update @bytelyst/<package> pnpm install pnpm typecheck # should compile clean
6. Failure modes + remediation
| Symptom | Likely cause | Fix |
|---|---|---|
| One Gitea publishes, the other doesn't | Push went only to one remote | git push gitea --tags && git push origin --tags |
| Byte-identical check fails | Different pnpm or Node version on the two runners | Pin both in workflow + Dockerfile digest |
pnpm publish says "version already exists" |
Re-running a tag | Either bump version or --force (not recommended) |
localhost:3300 unreachable from container |
container.network is bridge not host |
Set --network host in workflow options: or container.network: host in act_runner config.yaml |
| Release asset upload 401 | GITEA_NPM_TOKEN secret not configured at repo level |
Add it in Settings → Secrets |
| Lockfile drift between machines | Someone committed without running pnpm install |
Add pnpm install --frozen-lockfile as a pre-commit hook |
7. Deliverables for the agent implementing this
- File:
learning_ai_common_plat/.gitea/workflows/publish-packages.ymlwith the Node image digest pinned (rundocker pull node:20-bookworm && docker inspect node:20-bookworm | grep sha256to get the digest). - File:
learning_ai_common_plat/scripts/sync-publish-workflow.shif going with Option B. - Secret: confirm
GITEA_NPM_TOKENis set as a Gitea repo secret inSettings → Secrets. Document its value source (~/.gitea_npm_tokenon each machine). - Test: the throwaway-package E2E from
HOSTINGER_GITEA_ACT_RUNNER_SETUP.md§6 must pass, including the cross-Gitea SHA comparison. - Commit + push to both
origin(GitHub) andgitea(whichever Gitea is locally reachable). - Add a SKILL doc at
AI.dev/SKILLS/gitea-package-publishing.mdthat references this workflow and the act_runner setup.
8. Questions for the operator before implementing
- "Which packages should be auto-published on
v*tags? Currently I assume all@bytelyst/*packages inpackages/. Are there any to exclude?" - "Are we using version-tagged releases (e.g.,
v1.2.3triggers everything to that version) or changesets (per-package versions)? The workflow above assumes a single repo-wide tag." - "Should the workflow also run on
workflow_dispatchwith a specific package filter, or always publish everything that changed since the last tag?" - "What's the policy if the cross-Gitea SHA comparison fails for a real release? Block consumers from upgrading, or accept divergence with a warning?"
- "Is there a release approval gate before publishing (e.g., human-clicked workflow_dispatch confirmation), or is
git push gitea --tagsenough?"