diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7d9fd3..4da2903 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,24 @@ jobs: go-version: '1.25' cache: true + - name: gofmt (formatting gate) + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "These files are not gofmt-clean:"; echo "$unformatted" + echo "Run: gofmt -w ."; exit 1 + fi + + - name: go vet + run: go vet ./... + + - name: Cross-compile for prod target (linux/arm64) + # Production runs on a GH200 (linux/arm64). Pure-Go + CGO_ENABLED=0 + # means this is a clean cross-compile on the amd64 runner — no QEMU, + # no self-hosted runner. Catches a compile break on the prod arch + # that the host-arch test build would miss. + run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./cmd/cosift + - name: Run tests with coverage # cosift's internal/crawler hits ~25s; allow 5m total headroom. run: go test -race -coverprofile=coverage.out -covermode=atomic -timeout 5m ./... diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..070d714 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,148 @@ +name: deploy + +# Build + sign + publish a release artifact. PULL-BASED deploy: +# this workflow never touches the production box and never holds an SSH key. +# The box runs cosift-self-update.timer, which polls the latest GitHub Release, +# verifies sha256 + minisign signature against a public key baked on the box, +# snapshots, atomically swaps the binary, restarts, and health-gates with +# auto-rollback. See deploy/scripts/README.md. +# +# Triggers: +# - push of a version tag (v*) → cut a release for that tag +# - manual workflow_dispatch → build from a chosen ref (no release +# unless a tag is also present) + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write # needed to create the GitHub Release + upload assets + +jobs: + # (a) verify — gate the release on vet + race tests + a smoke subset. + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + cache: true + + - name: go vet + run: go vet ./... + + - name: Tests (race) + run: go test -race -timeout 5m ./... + + - name: Smoke subset (build + serve roundtrip) + # make smoke builds the binary, crawls, and asserts /healthz + /search. + # Guarded with a timeout so a hung crawl can't wedge the release. + run: timeout 300 make smoke + + # (b) build — reproducible static arm64 binary, sha256, minisign signature. + build: + needs: verify + runs-on: ubuntu-latest + outputs: + version: ${{ steps.ver.outputs.version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # full history so version stamping is meaningful + + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + cache: true + + - name: Resolve version stamp + id: ver + # Tag name when triggered by a tag push; otherwise the short SHA. + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + else + echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + fi + + - name: Build (linux/arm64, static, trimmed) + env: + CGO_ENABLED: '0' + GOOS: linux + GOARCH: arm64 + VERSION: ${{ steps.ver.outputs.version }} + # Stamp both main.version and internal/server.Version so /healthz and + # /metrics report the shipped version. Matches the Makefile `build` + # target's ldflags exactly (kept in sync deliberately). + run: | + go build -trimpath \ + -ldflags "-s -w -X main.version=${VERSION} -X github.com/pilot-protocol/cosift/internal/server.Version=${VERSION}" \ + -o cosift-linux-arm64 ./cmd/cosift + + - name: sha256 + run: sha256sum cosift-linux-arm64 | tee cosift-linux-arm64.sha256 + + - name: Install minisign + run: sudo apt-get update && sudo apt-get install -y minisign + + - name: Sign with minisign + env: + # Repo secret. The matching PUBLIC key is baked on the box at + # /etc/cosift/minisign.pub. Generate with `minisign -G` (see + # deploy/scripts/README.md). NEVER commit the secret key. + MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} + run: | + if [ -z "${MINISIGN_SECRET_KEY}" ]; then + echo "::error::MINISIGN_SECRET_KEY secret is not set" >&2 + exit 1 + fi + printf '%s' "${MINISIGN_SECRET_KEY}" > minisign.key + # -W: secret key is unencrypted (no interactive passphrase prompt). + # Trusted comment carries the version so the box can sanity-check it. + minisign -S -W -s minisign.key \ + -m cosift-linux-arm64 \ + -t "cosift ${{ steps.ver.outputs.version }} linux/arm64" + rm -f minisign.key + # Verification against the embedded public key is done on the box; + # here we just confirm the .minisig was produced. + test -f cosift-linux-arm64.minisig + + - uses: actions/upload-artifact@v7 + with: + name: cosift-release-artifacts + path: | + cosift-linux-arm64 + cosift-linux-arm64.sha256 + cosift-linux-arm64.minisig + retention-days: 30 + + # (c) release — publish the signed binary as a GitHub Release asset. + # Only on a tag push (workflow_dispatch builds the artifact but does not + # cut a release). + release: + needs: build + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v7 + with: + name: cosift-release-artifacts + + - name: Create / update GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: cosift ${{ github.ref_name }} + generate_release_notes: true + fail_on_unmatched_files: true + files: | + cosift-linux-arm64 + cosift-linux-arm64.sha256 + cosift-linux-arm64.minisig diff --git a/Makefile b/Makefile index 1ef0533..df9ef4e 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ smoke: @./scripts/smoke-test.sh build: - CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$(VERSION) -X github.com/calinteodor/cosift/internal/server.Version=$(VERSION)" -o $(BINARY) $(PKG) + CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$(VERSION) -X github.com/pilot-protocol/cosift/internal/server.Version=$(VERSION)" -o $(BINARY) $(PKG) # Light-touch verification: compile + vet + unit tests on packages that # don't need OPENAI/COHERE keys or live network. Catches latent compile diff --git a/cmd/cosift/assets/chat.html b/cmd/cosift/assets/chat.html index b8c1a0a..68a536a 100644 --- a/cmd/cosift/assets/chat.html +++ b/cmd/cosift/assets/chat.html @@ -213,6 +213,22 @@ font-family: var(--mono); font-size: 11px; padding: 6px 8px; cursor: pointer; } +.site-input-wrap { + display: flex; align-items: center; gap: 4px; + border: 1px solid var(--line); border-radius: 8px; + background: var(--bg-2); padding: 0 8px; +} +.site-input-label { + font-family: var(--mono); font-size: 10px; color: var(--ink-dim); + white-space: nowrap; user-select: none; flex-shrink: 0; +} +.site-input { + border: none; background: transparent; color: var(--ink); + font-family: var(--mono); font-size: 11px; + padding: 5px 0; width: 140px; min-width: 0; + outline: none; +} +.site-input::placeholder { color: var(--ink-dim); opacity: 0.6; } .send-btn { padding: 14px 22px; background: var(--accent); color: var(--accent-ink); @@ -324,6 +340,10 @@

Chat with the corpus.

+
+ site: + +