diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..db326df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI + +on: + pull_request: + branches: [development] + push: + branches: [development, main] + +jobs: + continuous-integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint markdown + run: | + for f in $(find . -name "*.md"); do + echo "Checking $f..." + test -s "$f" || echo " WARNING: empty file" + done diff --git a/.github/workflows/coordinated-release.yml b/.github/workflows/coordinated-release.yml new file mode 100644 index 0000000..5fe526b --- /dev/null +++ b/.github/workflows/coordinated-release.yml @@ -0,0 +1,21 @@ +name: Coordinated Release + +on: + workflow_dispatch: + inputs: + version: + description: 'SemVer tag (e.g. v1.0.0-alpha)' + required: true + message: + description: 'Tag message' + required: true + default: 'CognitiveOS coordinated release' + +jobs: + tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: scripts/release-tag.sh "${{ inputs.version }}" "${{ inputs.message }}" + env: + COGNITIVEOS_RELEASE_DIR: /tmp/cognitiveos-releases diff --git a/.github/workflows/healthcheck.yml b/.github/workflows/healthcheck.yml new file mode 100644 index 0000000..25f6556 --- /dev/null +++ b/.github/workflows/healthcheck.yml @@ -0,0 +1,54 @@ +name: Health Check + +on: + schedule: + - cron: '0 6 * * 1' # Every Monday 06:00 UTC + workflow_dispatch: + +jobs: + healthcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run health check + id: check + run: | + scripts/healthcheck.sh \ + --work-dir /tmp/cognitiveos-healthcheck \ + --branch main \ + --json \ + --quiet > /tmp/healthcheck-result.json 2>/dev/null + + - name: Upload report + uses: actions/upload-artifact@v4 + with: + name: healthcheck-report + path: /tmp/healthcheck-result.json + + - name: Check results + id: results + run: | + fail=$(jq -r '.summary.fail' /tmp/healthcheck-result.json 2>/dev/null || echo 1) + if [ "$fail" -gt 0 ]; then + echo "failures=$fail" >> "$GITHUB_OUTPUT" + echo "Issues found: $fail check(s) failed" >> /tmp/healthcheck-summary.txt + jq -r '.checks[] | select(.status == "fail") | " FAIL: \(.name) — \(.detail)"' \ + /tmp/healthcheck-result.json >> /tmp/healthcheck-summary.txt + fi + + - name: Create issue on failure + if: steps.results.outputs.failures && fromJson(steps.results.outputs.failures) > 0 + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const body = fs.readFileSync('/tmp/healthcheck-summary.txt', 'utf8'); + const date = new Date().toISOString().split('T')[0]; + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Health check failure — ${date}`, + body: `## Health Check Failed\n\n${body}\n\nSee workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + labels: ['healthcheck'] + }); diff --git a/README.md b/README.md index d55a7a0..a1fff0e 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,14 @@ Software Development Life Cycle documentation for the CognitiveOS project. ## Contributing -1. Branch from `development`, not `main` -2. Use topic branches: `feature/`, `fix/`, `bugfix/` -3. Open a PR to `development` with a clear title and description -4. Merge via squash after review -5. Changes flow to `main` via a release PR +Read the guides in `workflow/` before submitting changes: -See the [SDLC repo](https://github.com/CognitiveOS-Project/sdlc) for the full contribution guide, code review standards, and testing strategy. +- [`workflow/contribution-guide.md`](workflow/contribution-guide.md) — full contribution process +- [`workflow/code-review.md`](workflow/code-review.md) — review checklist and expectations +- [`workflow/testing.md`](workflow/testing.md) — testing strategy across all layers +- [`workflow/ci-cd.md`](workflow/ci-cd.md) — CI/CD pipeline definitions + +All repos in the CognitiveOS project follow topic-branch flow: branch from `development`, PR to `development`, squash merge, then release PR to `main`. ## Author diff --git a/plan/implementation-plan.md b/plan/implementation-plan.md index a19da58..1438965 100644 --- a/plan/implementation-plan.md +++ b/plan/implementation-plan.md @@ -21,7 +21,7 @@ This plan covers the implementation of all 10 repos in the CognitiveOS-Project o | `cli` | UI | Go | Bubble Tea TUI frontend | | `cognitiveos-distro` | Distribution | Shell/Docker | Alpine image builder | | `cgp-template` | Ecosystem | Template | .cgp skill boilerplate | -| `registry-server` | Infrastructure | Go | .cgp package registry | +| `registry-server` | Infrastructure | Go | .cgp notary proxy (metadata + checksum, no file hosting) | ## Dependency Graph @@ -41,7 +41,7 @@ product-specs ────────────────────── │ │ ├── cli ────────── depends on cognitiveosd (socket client) │ - ├── registry-server ─── depends on registry-api spec + ├── registry-server ─── depends on registry-api spec, dependency-validation spec │ └── cgp-template ─ depends on .cgp format spec @@ -135,25 +135,36 @@ cognitiveos-distro ───── depends on all built binaries **Repos:** `inference` -**Goal:** Working inference engine that can load and run GGUF models, exposed via an Ollama-compatible API. +**Goal:** Working inference engine that can load and run GGUF models, exposed via an Ollama-compatible API. Architecture uses a **vendored llama.cpp C source + thin CGo bridge** — no `llama-cli` subprocess. | Task | Dependencies | Est. effort | |------|-------------|-------------| -| llama.cpp cross-compilation for Alpine | None | Medium | -| `POST /api/generate` | None | Medium | -| `POST /api/chat` | None | Medium | +| Vendor `llama.cpp` git submodule (pinned commit) | None | Small | +| `bridge.go` — single `import "C"` file wrapping llama.h API | None | Medium | +| `cgobackend.go` — `CgoBackend` struct implementing `Backend` interface | bridge.go | Medium | +| `loadopts.go` — `LoadOptions{NumCtx, GPULayers, Threads}` | None | Small | +| cmake + gcc toolchain in `Dockerfile.build` | None | Medium | +| Register `--backend cgo` in `cmd/coginfer/main.go` | cgobackend.go | Small | +| `POST /api/generate` — CGo-backed inference | server.go, bridge.go | Medium | +| `POST /api/chat` | generate handler | Small | | `GET /api/tags` — model listing | None | Small | -| `GET /cognitiveos/status` — resource reporting | inference-api spec | Small | +| `GET /cognitiveos/status` — resource reporting (CGo stats) | inference-api spec | Small | | `GET /cognitiveos/capabilities` — hardware detection | inference-api spec | Small | +| Wire `LoadOptions` from HTTP request params to `backend.Load()` | server.go | Small | | Resource negotiation with cognitiveosd | cognitiveosd-api | Medium | -| Idle timeout and auto-unload | None | Small | -| Model swap (unload old, load new) | None | Medium | +| Idle timeout and auto-unload (calls `CgoBackend.Unload()`) | None | Small | +| Model swap (unload old, load new via CGo) | None | Medium | +| Fix `cmd/cograw/main.go` bugs — undefined `llamaBin`, `verifyModel()`, `ramMB` | None | Small | +| Replace `exec.Command("llama-cli")` in cograw with CGo bridge | bridge.go | Medium | +| JSON-RPC 2.0 handler for Raw Model (wrapping bridge calls) | None | Medium | **Definition of done:** -- Raw Model loads from `/cognitiveos/models/raw/raw-model.gguf` -- `POST /api/generate` produces coherent completions +- Raw Model loads from `/cognitiveos/models/raw/raw-model.gguf` via CGo bridge (no subprocess) +- `POST /api/generate` produces coherent completions via CGo - Resource usage reported correctly in `/cognitiveos/status` - Inference engine communicates with cognitiveosd for load/unload +- `CLIBackend` and `--backend cli` flag removed (Phase 5) +- `--backend` defaults to `"cgo"` in production (CGO_ENABLED=1) and `"mock"` when CGO_ENABLED=0 ### Phase 4: System Daemon @@ -240,26 +251,38 @@ cognitiveos-distro ───── depends on all built binaries **Repos:** `registry-server`, `cgp-template` -**Goal:** Usable package registry and developer template. - -| Task | Dependencies | Est. effort | -|------|-------------|-------------| -| `GET /v1/search` | registry-api | Medium | -| `GET /v1/patches/{name}/{version}` | registry-api | Small | -| `GET .../download` — .cgp binary streaming | registry-api | Medium | -| `POST /v1/patches` — publish with auth | registry-api | Medium | -| `POST .../unlock` — unlock code verification | registry-api | Medium | -| SQLite metadata index | None | Medium | -| Token-based auth | None | Medium | -| cgp-template with sample patch | cgp-format spec | Small | -| cgp-template README and documentation | None | Small | +**Goal:** Usable package notary proxy and developer template. + +The registry is a **notary proxy** — it does not host `.cgp` files. Publishers provide a canonical `download_url` and a `sha256` checksum; the registry stores metadata and redirects clients to the download URL. This avoids file storage scaling concerns and allows publishers to host archives on their own infrastructure (GitHub Releases, S3, etc.). + +| Task | Dependencies | Est. effort | Status | +|------|-------------|-------------|--------| +| `GET /v1/search` — text search across name, description, tags | registry-api | Small | ✅ Done | +| `GET /v1/patches/{name}` — latest version metadata | registry-api | Small | ✅ Done | +| `GET /v1/patches/{name}/{version}` — version-specific metadata with full manifest | registry-api | Small | ✅ Done | +| `GET /v1/patches/{name}/{version}/download` — HTTP 302 redirect to `download_url` | registry-api | Small | ✅ Done | +| `POST /v1/patches` — JSON-only publish with `manifest`, `sha256`, `download_url` | registry-api | Medium | ✅ Done | +| `PUT /v1/patches/{name}/{version}` — publish new version, URL validated against body | registry-api | Small | ✅ Done | +| A1-A10 publish-time validation (manifest parse, schema, SHA-256 format, dep cycles, file refs, hardware bounds, URL reachability) | dependency-validation spec | Large | ✅ Done | +| Scoped token auth (`publish` scope for POST/PUT, `admin` scope for status/validate) | None | Medium | ✅ Done | +| `PATCH /v1/patches/{name}/{version}/status` — set active/deprecated/buggy | registry-api | Small | ✅ Done | +| `POST /v1/patches/{name}/{version}/validate` — re-run A1-A10 on stored manifest | registry-api | Small | ✅ Done | +| `GET /v1/patches/{name}/dependencies` — dependency tree for a package | registry-api | Small | ✅ Done | +| File-backed store (JSON file, survives restarts, SQLite adapter interface ready) | None | Medium | ✅ Done | +| `POST .../unlock` — unlock code verification | registry-api | Medium | Partial | +| SQLite metadata index (upgrade from file-backed JSON) | None | Medium | Pending | +| cgp-template with sample patch | cgp-format spec | Small | ✅ Done | +| cgp-template README and documentation | None | Small | ✅ Done | +| Registry API spec documenting notary proxy (no file hosting) | — | Small | ✅ Done | +| `publish-cgp.sh` updated for notary pattern (JSON + sha256 + download-url) | — | Small | ✅ Done | **Definition of done:** -- `cpm search email` returns results from the registry -- `cpm publish ./skill.cgp` uploads to the registry -- `cpm install ` downloads and installs from registry -- Unlock code flow works end-to-end -- `cpm init my-skill` creates a valid .cgp skeleton +- `cpm search email` returns results from the registry ✅ +- `cpm publish ./skill.cgp --download-url ` registers checksum in registry ✅ +- `cpm install ` downloads from `download_url` after registry redirect ✅ +- Unlock code flow works end-to-end ⬜ (stub implementation) +- A1-A10 validation rejects malformed publishes at registry level ✅ +- `cpm init my-skill` creates a valid .cgp skeleton ✅ ## Build Order with Milestones diff --git a/plan/milestones.md b/plan/milestones.md index c917e19..9dfbf3e 100644 --- a/plan/milestones.md +++ b/plan/milestones.md @@ -45,12 +45,21 @@ - [ ] Raw Model loads at boot - [ ] **Demo:** Boot CognitiveOS in QEMU, type a command, get a response -## M6 — Registry Ecosystem -- [ ] `GET /v1/search` returns results -- [ ] `cpm publish ./skill.cgp` uploads to registry -- [ ] `cpm install ` downloads from registry -- [ ] Unlock code flow works end-to-end -- [ ] `cpm init my-skill` creates valid .cgp skeleton +## M6 — Registry Ecosystem (Notary Proxy) +- [x] `GET /v1/search` returns results +- [x] `GET /v1/patches/{name}/{version}` returns metadata with sha256 + download_url +- [x] `GET /v1/patches/{name}/{version}/download` redirects to canonical download URL +- [x] `POST /v1/patches` JSON-only publish with manifest + sha256 + download_url +- [x] `PUT /v1/patches/{name}/{version}` publish new version +- [x] A1-A10 publish-time validation (manifest, schema, cycles, file refs, hardware bounds, URLs) +- [x] Scoped token auth (publish/admin) +- [x] File-backed persistence (survives restarts) +- [x] `PATCH .../status`, `POST .../validate`, `GET .../dependencies` endpoints +- [x] `cpm publish ./skill.cgp --download-url ` registers in registry +- [x] `cpm install ` downloads via registry redirect +- [x] `cpm init my-skill` creates valid .cgp skeleton +- [ ] SQLite backend (upgrade from file-backed JSON) +- [ ] Full unlock code flow end-to-end - [ ] **Demo:** `cpm search photo` → `cpm install photo-viewer` → AI can show photos ## M7 — v0.1.0 Release diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh new file mode 100755 index 0000000..7df1ce5 --- /dev/null +++ b/scripts/healthcheck.sh @@ -0,0 +1,389 @@ +#!/bin/sh +set -e + +# ── Config ── +ALL_REPOS="cognitiveos product-specs sdlc cpm core-mcp-bridges inference cognitiveosd cli cognitiveos-distro registry-server cgp-template" + +GO_REPOS="inference cognitiveosd cli cpm core-mcp-bridges registry-server" +SHELL_REPOS="cognitiveos-distro sdlc" +CGO_REPOS="inference" + +CURRENT_TAG="v0.4.0" + +# ── Colors ── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' + +# ── State ── +WORK_DIR="" +REPORT_DIR="" +BRANCH="main" +MODE_JSON=0 +MODE_QUIET=0 +pass=0 +fail=0 +skip=0 +report="" +json_entries="" + +# ── Functions ── +usage() { + cat </dev/null + git -C "$dir" fetch origin --tags --quiet 2>/dev/null || true + git -C "$dir" checkout -f "origin/$BRANCH" --quiet 2>/dev/null + return 0 + fi + git clone --depth=1 "git@github.com:CognitiveOS-Project/${repo}.git" "$dir" --branch "$BRANCH" --quiet 2>/dev/null + git -C "$dir" fetch origin --tags --quiet 2>/dev/null || true +} + +has_go() { command -v go >/dev/null 2>&1; } +has_shellcheck() { command -v shellcheck >/dev/null 2>&1; } + +check_go_build() { + local dir="$1" label="$2" + if ! has_go; then + check "go build $label" skip "go not installed" + return + fi + if CGO_ENABLED=0 go build -o /dev/null ./... 2>&1; then + check "go build $label" pass + else + check "go build $label" fail "build error" + fi +} + +check_go_vet() { + local dir="$1" label="$2" + if ! has_go; then + check "go vet $label" skip "go not installed" + return + fi + if go vet ./... 2>&1; then + check "go vet $label" pass + else + check "go vet $label" fail "vet errors" + fi +} + +check_sh_syntax() { + local dir="$1" label="$2" + local errors=0 + for script in "$dir"/scripts/*.sh; do + [ -f "$script" ] || continue + sh -n "$script" 2>/dev/null || { errors=$((errors+1)); } + done + if [ "$errors" -eq 0 ]; then + check "sh -n scripts/ $label" pass + else + check "sh -n scripts/ $label" fail "$errors script(s) with syntax errors" + fi +} + +check_shellcheck() { + local dir="$1" label="$2" + if ! has_shellcheck; then + check "shellcheck $label" skip "not installed" + return + fi + local errors=0 + for script in "$dir"/scripts/*.sh; do + [ -f "$script" ] || continue + shellcheck -s sh -S warning "$script" 2>/dev/null || errors=$((errors+1)) + done + if [ "$errors" -eq 0 ]; then + check "shellcheck $label" pass + else + check "shellcheck $label" fail "$errors script(s) with warnings" + fi +} + +check_build_tags() { + local dir="$1" label="$2" + local errors=0 + for f in bridge.go cgobackend.go; do + p="$dir/internal/llm/$f" + [ -f "$p" ] || continue + head -1 "$p" | grep -q 'cgo' || { errors=$((errors+1)); check "build tag cgo on $f ($label)" fail "missing //go:build cgo"; } + done + for f in cograw_llm.go; do + p="$dir/cmd/cograw/$f" + [ -f "$p" ] || continue + head -1 "$p" | grep -q 'cgo' || { errors=$((errors+1)); check "build tag cgo on $f ($label)" fail "missing //go:build cgo"; } + done + for f in cograw_stub.go backend_stub.go; do + for d in cmd/cograw internal/server; do + p="$dir/$d/$f" + [ -f "$p" ] || continue + head -1 "$p" | grep -q '!cgo' || { errors=$((errors+1)); check "build tag !cgo on $f ($label)" fail "missing //go:build !cgo"; } + done + done + if [ "$errors" -eq 0 ]; then + check "build tags ($label)" pass + fi + unset errors +} + +check_stale_refs() { + local dir="$1" label="$2" + if grep -qr 'llamaBin\|CLIBackend\|llama-cli' "$dir" --include='*.go' 2>/dev/null; then + check "stale refs ($label)" fail "llamaBin/CLIBackend/llama-cli found" + else + check "stale refs ($label)" pass + fi +} + +check_dockerfile() { + local dir="$1" label="$2" + local df="" + for candidate in "$dir/docker/Dockerfile.build" "$dir/Dockerfile"; do + [ -f "$candidate" ] && df="$candidate" && break + done + [ -z "$df" ] && { check "Dockerfile ($label)" skip "not found"; return; } + if grep -q '^FROM ' "$df" 2>/dev/null; then + check "Dockerfile FROM ($label)" pass + else + check "Dockerfile FROM ($label)" fail "missing FROM" + fi + if grep -q 'https://github.com' "$df" 2>/dev/null; then + check "Dockerfile SSH ($label)" fail "HTTPS clone found" + else + check "Dockerfile SSH ($label)" pass + fi +} + +check_cgo_enabled() { + local dir="$1" label="$2" + if grep -q 'CGO_ENABLED=1' "$dir/scripts/build-binaries.sh" 2>/dev/null; then + check "CGO_ENABLED=1 ($label)" pass + else + check "CGO_ENABLED=1 ($label)" fail "not set in build-binaries.sh" + fi +} + +check_cograw_target() { + local dir="$1" label="$2" + if grep -q 'cograw' "$dir/scripts/build-binaries.sh" 2>/dev/null; then + check "cograw target ($label)" pass + else + check "cograw target ($label)" fail "not found in build-binaries.sh" + fi +} + +# ── Parse args ── +while [ $# -gt 0 ]; do + case "$1" in + --work-dir) shift; WORK_DIR="$1" ;; + --report-dir) shift; REPORT_DIR="$1" ;; + --branch) shift; BRANCH="$1" ;; + --json) MODE_JSON=1 ;; + --quiet) MODE_QUIET=1 ;; + --help|-h) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac + shift +done + +# ── Setup directories ── +if [ -z "$WORK_DIR" ]; then + WORK_DIR="$(mktemp -d /tmp/cognitiveos-healthcheck-XXXXXX)" + CLEANUP_WORK=1 +else + mkdir -p "$WORK_DIR" + CLEANUP_WORK=0 +fi +[ -z "$REPORT_DIR" ] && REPORT_DIR="./reports" +mkdir -p "$REPORT_DIR" + +START_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) +REPORT_FILE="${REPORT_DIR}/healthcheck-${BRANCH}-$(date +%Y%m%d-%H%M%S).txt" + +# ── Preamble ── +section "CognitiveOS Health Check" +[ "$MODE_QUIET" -eq 0 ] && report="${report}Branch: ${BRANCH}\nTag: ${CURRENT_TAG}\nDate: $(date)\nHost: $(uname -a)\n" +hr + +# ── Tool availability ── +section "Tool Availability" +for tool in git go shellcheck; do + if command -v "$tool" >/dev/null 2>&1; then + ver=$("$tool" version 2>&1 | head -1) + check "$tool" pass "$ver" + else + check "$tool" skip "not installed" + fi +done +hr + +# ── Clone / update all repos ── +section "Repository Checkout" +errors=0 +for repo in $ALL_REPOS; do + if clone_repo "$repo" "$WORK_DIR/$repo"; then + check "clone $repo" pass + else + check "clone $repo" fail "clone failed" + errors=$((errors+1)) + fi +done +if [ "$errors" -gt 0 ]; then + [ "$MODE_QUIET" -eq 0 ] && report="${report}\n${RED}CRITICAL: some repos failed to clone — skipping their checks${NC}\n" +fi +hr + +# ── Per-repo checks ── +for repo in $ALL_REPOS; do + dir="$WORK_DIR/$repo" + [ -d "$dir/.git" ] || continue + + section "$repo" + + # Git status + if [ -z "$(git -C "$dir" status --porcelain)" ]; then + check "git status clean" pass + else + check "git status clean" fail "uncommitted changes" + fi + + # Branch match + local_branch=$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached") + if [ "$local_branch" = "$BRANCH" ]; then + check "on branch $BRANCH" pass + else + check "on branch $BRANCH" fail "on $local_branch" + fi + + # Tag present + if git -C "$dir" tag -l "$CURRENT_TAG" 2>/dev/null | grep -q .; then + check "tag $CURRENT_TAG" pass + else + git -C "$dir" fetch origin --tags --quiet 2>/dev/null || true + if git -C "$dir" tag -l "$CURRENT_TAG" 2>/dev/null | grep -q .; then + check "tag $CURRENT_TAG" pass + else + check "tag $CURRENT_TAG" fail "missing" + fi + fi + + # Go repo checks + case " $GO_REPOS " in + *" $repo "*) + check_go_build "$dir" "$repo" + check_go_vet "$dir" "$repo" + ;; + esac + + # CGo-specific checks + case " $CGO_REPOS " in + *" $repo "*) + check_build_tags "$dir" "$repo" + check_stale_refs "$dir" "$repo" + ;; + esac + + # Shell script checks + case " $SHELL_REPOS " in + *" $repo "*) + check_sh_syntax "$dir" "$repo" + check_shellcheck "$dir" "$repo" + ;; + esac + + # Dockerfile checks + check_dockerfile "$dir" "$repo" + + # cognitiveos-distro specific + if [ "$repo" = "cognitiveos-distro" ]; then + check_cgo_enabled "$dir" "$repo" + check_cograw_target "$dir" "$repo" + fi +done +hr + +# ── Summary ── +summary_human="\n${BOLD}Summary${NC}\n Pass: ${GREEN}${pass}${NC}\n Fail: ${RED}${fail}${NC}\n Skip: ${YELLOW}${skip}${NC}\n Total: $((pass+fail+skip))\n" +[ "$MODE_QUIET" -eq 0 ] && report="${report}${summary_human}" + +# Print human report to stderr +printf "%b\\n" "$report" >&2 + +# Write report file +printf "%b\\n" "$report" > "$REPORT_FILE" +echo "Report: $REPORT_FILE" >&2 + +# JSON output to stdout +if [ "$MODE_JSON" -eq 1 ]; then + cat </dev/null | sort -V | tail -1) + echo " $repo ${latest:-no tags}" + else + echo " $repo (not cloned)" + fi + done +} + +if [ "${1:-}" = "--list" ]; then + list_tags + exit 0 +fi + +# --- argument parsing --- +DRY_RUN=false +if [ "${1:-}" = "--dry-run" ]; then + DRY_RUN=true + shift +fi + +if [ $# -lt 2 ]; then + echo "Usage: $0 [--dry-run] " + echo " $0 --list" + echo "" + echo "Examples:" + echo " $0 v1.0.0-alpha \"System foundations complete\"" + echo " $0 --dry-run v1.0.0-alpha \"System foundations complete\"" + echo " $0 --list" + exit 1 +fi + +VERSION="$1" +MSG="$2" + +if ! echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then + echo "Error: version must match semver format (e.g. v1.0.0-alpha)" + exit 1 +fi + +# --- header --- +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Coordinated Release Tag" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Version: $VERSION" +echo " Message: $MSG" +echo " Cache: $BASE_DIR" +[ "$DRY_RUN" = true ] && echo " Mode: DRY RUN (no changes)" +echo "" + +# --- ensure base dir --- +if [ "$DRY_RUN" = false ] && ! mkdir -p "$BASE_DIR" 2>/dev/null; then + echo "Error: cannot create base directory $BASE_DIR" + echo "Set COGNITIVEOS_RELEASE_DIR to a writable path" + exit 1 +fi + +FAILED=0 +SUCCEEDED=0 +RESULTS_FILE=$(mktemp) + +for repo in $REPOS; do + target="$BASE_DIR/$(repo_dir "$repo")" + repo_label=$(printf "%-18s" "$repo") + + if [ "$DRY_RUN" = true ]; then + echo " $repo_label~ tag $VERSION (dry-run)" + echo "$repo ~ tag $VERSION" >> "$RESULTS_FILE" + SUCCEEDED=$((SUCCEEDED + 1)) + continue + fi + + # clone if not present + if [ ! -d "$target/.git" ]; then + if ! gh repo clone "${ORG}/${repo}" "$target" 2>/dev/null; then + echo " $repo_label✗ clone failed" + echo "$repo ✗ clone failed" >> "$RESULTS_FILE" + FAILED=$((FAILED + 1)) + continue + fi + fi + + # fetch, tag, push — subshell for isolation + if (cd "$target" && \ + git fetch --tags --force origin 2>/dev/null && \ + git tag -a "$VERSION" -m "$MSG" 2>/dev/null && \ + git push origin "$VERSION" 2>/dev/null); then + echo " $repo_label✓ $VERSION" + echo "$repo ✓ $VERSION" >> "$RESULTS_FILE" + SUCCEEDED=$((SUCCEEDED + 1)) + else + # tag already exists → skip, not error + if (cd "$target" && git tag -l "$VERSION" 2>/dev/null | grep -qFx "$VERSION"); then + echo " $repo_label- already exists" + echo "$repo - already exists" >> "$RESULTS_FILE" + SUCCEEDED=$((SUCCEEDED + 1)) + else + echo " $repo_label✗ failed" + echo "$repo ✗ failed" >> "$RESULTS_FILE" + FAILED=$((FAILED + 1)) + fi + fi +done + +# --- summary --- +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " RESULTS" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +sort "$RESULTS_FILE" +rm -f "$RESULTS_FILE" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " $SUCCEEDED succeeded, $FAILED failed" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +[ "$FAILED" -gt 0 ] && exit 1 || exit 0 diff --git a/workflow/ci-cd.md b/workflow/ci-cd.md index a2c92d1..8598c95 100644 --- a/workflow/ci-cd.md +++ b/workflow/ci-cd.md @@ -86,7 +86,7 @@ jobs: path: output/*.iso ``` -## Release Workflow +## Release Workflow (Per Repo) File: `.github/workflows/release.yml` @@ -111,6 +111,49 @@ jobs: cognitiveos-*.img ``` +## Coordinated Release (Cross-Repo) + +At the end of a release cycle, all 13 repos must be tagged at the same SemVer version. Use the `release-tag.sh` script: + +```bash +scripts/release-tag.sh v1.0.0-alpha "v1.0.0-alpha — System foundations complete" +``` + +The script: +- Clones each repo on demand into a persistent cache directory +- Creates **annotated tags** (compliant with release-strategy.md) +- Skips repos where the tag already exists (idempotent) +- Reports per-repo status in a summary table +- Exits non-zero if any repo fails + +### Manual Trigger (GitHub Actions) + +File: `.github/workflows/coordinated-release.yml` + +```yaml +name: Coordinated Release + +on: + workflow_dispatch: + inputs: + version: + description: 'SemVer tag (e.g. v1.0.0-alpha)' + required: true + message: + description: 'Tag message' + required: true + default: 'CognitiveOS coordinated release' + +jobs: + tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: scripts/release-tag.sh "${{ inputs.version }}" "${{ inputs.message }}" + env: + COGNITIVEOS_RELEASE_DIR: /tmp/cognitiveos-releases +``` + ## Status Badges Each repo README should include: diff --git a/workflow/contribution-guide.md b/workflow/contribution-guide.md index 0f585de..3040963 100644 --- a/workflow/contribution-guide.md +++ b/workflow/contribution-guide.md @@ -20,7 +20,7 @@ git commit -m ": " git push -u origin HEAD ``` -Create a PR into `development`. See [git-workflow.md](https://github.com/jeanmachuca/aiworkspace/blob/main/.opencode/instructions/git-workflow.md) for full details. +Create a PR into `development`. See the root `AGENTS.md` and `.opencode/instructions/git-workflow.md` in the workspace root for full details. Commit types: - `feat:` — New feature