diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3efbc4a..a4b573f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,3 +85,74 @@ jobs: with: sarif_file: trivy-fs.sarif category: trivy-fs + + integration: + name: Integration + runs-on: ubuntu-latest + needs: test + + steps: + - uses: actions/checkout@v6 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Build NORA + run: cargo build --release --package nora-registry + + # -- Start NORA -- + - name: Start NORA + run: | + NORA_STORAGE_PATH=/tmp/nora-data ./target/release/nora & + for i in $(seq 1 15); do + curl -sf http://localhost:4000/health && break || sleep 2 + done + curl -sf http://localhost:4000/health | jq . + + # -- Docker push/pull -- + - name: Configure Docker for insecure registry + run: | + echo '{"insecure-registries": ["localhost:4000"]}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker + sleep 2 + + - name: Docker — push and pull image + run: | + docker pull alpine:3.20 + docker tag alpine:3.20 localhost:4000/test/alpine:integration + docker push localhost:4000/test/alpine:integration + docker rmi localhost:4000/test/alpine:integration + docker pull localhost:4000/test/alpine:integration + echo "Docker push/pull OK" + + - name: Docker — verify catalog and tags + run: | + curl -sf http://localhost:4000/v2/_catalog | jq . + curl -sf http://localhost:4000/v2/test/alpine/tags/list | jq . + + # -- npm publish/install -- + - name: npm — publish and install package + run: | + mkdir -p /tmp/test-pkg && cd /tmp/test-pkg + echo '{"name":"nora-integration-test","version":"1.0.0","description":"test"}' > package.json + echo "module.exports = true;" > index.js + npm publish --registry http://localhost:4000/npm/ + cd /tmp && mkdir -p install-test && cd install-test + npm init -y > /dev/null + npm install nora-integration-test --registry http://localhost:4000/npm/ + echo "npm publish/install OK" + + # -- API checks -- + - name: API — health, ready, metrics + run: | + curl -sf http://localhost:4000/health | jq .status + curl -sf http://localhost:4000/ready + curl -sf http://localhost:4000/metrics | head -5 + echo "API checks OK" + + - name: Stop NORA + if: always() + run: pkill nora || true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 620a964..af3c65e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: push: true tags: ${{ steps.meta-alpine.outputs.tags }} labels: ${{ steps.meta-alpine.outputs.labels }} - cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:alpine + cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:alpine,ignore-error=true cache-to: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:alpine,mode=max # ── RED OS ─────────────────────────────────────────────────────────────── @@ -98,7 +98,7 @@ jobs: push: true tags: ${{ steps.meta-redos.outputs.tags }} labels: ${{ steps.meta-redos.outputs.labels }} - cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:redos + cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:redos,ignore-error=true cache-to: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:redos,mode=max # ── Astra Linux SE ─────────────────────────────────────────────────────── @@ -124,7 +124,7 @@ jobs: push: true tags: ${{ steps.meta-astra.outputs.tags }} labels: ${{ steps.meta-astra.outputs.labels }} - cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:astra + cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:astra,ignore-error=true cache-to: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:astra,mode=max # ── Smoke test ────────────────────────────────────────────────────────── diff --git a/RELEASE_RUNBOOK.md b/RELEASE_RUNBOOK.md new file mode 100644 index 0000000..edfd0b8 --- /dev/null +++ b/RELEASE_RUNBOOK.md @@ -0,0 +1,80 @@ +# Release Runbook + +## Release process + +1. Update version in `nora-registry/Cargo.toml` +2. Update `CHANGELOG.md` +3. Commit: `chore: bump version to X.Y.Z` +4. Tag: `git tag vX.Y.Z && git push origin vX.Y.Z` +5. CI builds binary + 3 Docker images (alpine, redos, astra) +6. CI runs trivy scan on all images +7. CI creates GitHub Release with binary, checksums, SBOM + +## Deploy order + +1. **ai-server** (internal) — update first, verify +2. **PROD** — update after ai-server is stable +3. **GHCR** — public images pushed by CI automatically + +## Rollback + +### Quick rollback (revert to previous version) + +```bash +# On ai-server +docker pull ghcr.io/getnora-io/nora:PREVIOUS_VERSION +docker stop nora && docker rm nora +docker run -d --name nora -p 4000:4000 \ + -v /srv/nora-data:/data \ + ghcr.io/getnora-io/nora:PREVIOUS_VERSION +``` + +### Delete a broken release + +```bash +# 1. Delete GitHub Release (keeps tag) +gh release delete vX.Y.Z --yes + +# 2. Delete tag +git tag -d vX.Y.Z +git push origin :refs/tags/vX.Y.Z + +# 3. Delete GHCR images (all variants) +for suffix in "" "-redos" "-astra"; do + gh api -X DELETE /user/packages/container/nora/versions \ + --jq ".[] | select(.metadata.container.tags[] | contains(\"X.Y.Z${suffix}\")) | .id" \ + | xargs -I{} gh api -X DELETE /user/packages/container/nora/versions/{} +done +``` + +### Binary rollback + +```bash +curl -LO https://github.com/getnora-io/nora/releases/download/vPREVIOUS/nora-linux-amd64 +chmod +x nora-linux-amd64 +sudo mv nora-linux-amd64 /usr/local/bin/nora +sudo systemctl restart nora +``` + +## Verification after deploy + +```bash +# Health check +curl -sf http://localhost:4000/health | jq . + +# Docker API +curl -sf http://localhost:4000/v2/ | jq . + +# Push test image +docker pull alpine:3.20 +docker tag alpine:3.20 localhost:4000/test/alpine:smoke +docker push localhost:4000/test/alpine:smoke +docker pull localhost:4000/test/alpine:smoke +``` + +## Known issues + +- Self-hosted runner uses localhost:5000 (NORA) for buildx cache. + If NORA is down during release, build continues without cache (ignore-error=true). +- Trivy image scan runs after push to localhost:5000 but before GitHub Release. + A failed scan blocks the release.