diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..4272705 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,118 @@ +name: Docker + +on: + push: + branches: [main] + tags: ["v*.*.*"] + pull_request: + branches: [main] + # Manual republish (e.g. base-image refresh without a version bump). + # Must be dispatched FROM A TAG REF: metadata-action only derives semver + # image tags from the git tag, so a branch dispatch is rejected below. + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + # Fail fast with a clear message instead of silently pushing a bare + # `latest` (or nothing) when someone dispatches from a branch. + guard-dispatch: + if: github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - run: | + echo "::error::Manual runs must be dispatched from a v*.*.* tag ref (use the 'Run workflow' ref selector). Branch refs produce no semver image tags." + exit 1 + + # Catch tag/code drift before anything is published (root.go has drifted + # from tags before). + verify-version: + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check tag matches cmd/root.go version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + CODE_VERSION=$(sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' cmd/root.go) + echo "tag=v${TAG_VERSION} code=${CODE_VERSION}" + if [ "$TAG_VERSION" != "$CODE_VERSION" ]; then + echo "::error::Git tag v${TAG_VERSION} does not match version \"${CODE_VERSION}\" in cmd/root.go. Bump cmd/root.go before tagging." + exit 1 + fi + + # PR / main validation: build only, single platform (multi-arch under QEMU + # roughly doubles CI time for no extra signal; tag builds cover arm64). + build: + if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build (no push) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + push: false + cache-from: type=gha + cache-to: type=gha,mode=max + + # Publish on version tags (and manual dispatch from a tag ref). + # No fork-secrets concern: this never runs for pull_request events. + publish: + if: startsWith(github.ref, 'refs/tags/v') + needs: verify-version + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # `latest` is applied automatically on semver tags (latest=auto). + # Caveat: it follows the most recently pushed tag, not the highest + # version — we only tag forward, never backport. + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: karust/openserp + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + # The pinned chromedp/headless-shell digest is a multi-arch OCI index + # (amd64 + arm64/v8), and the builder stage cross-compiles via + # $BUILDPLATFORM, so only the small final-stage apt layer runs under QEMU. + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index cf1da5e..319324e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/karust/openserp)](https://goreportcard.com/report/github.com/karust/openserp) [![Go Reference](https://pkg.go.dev/badge/github/karust/openserp?style=for-the-badge)](https://pkg.go.dev/github.com/karust/openserp) [![release](https://img.shields.io/github/v/release/karust/openserp)](https://github.com/karust/openserp/releases) -[![Docker Pulls](https://img.shields.io/docker/v/karust/openserp)](https://hub.docker.com/repository/docker/karust/openserp) +[![Docker Pulls](https://img.shields.io/docker/v/karust/openserp)](https://hub.docker.com/r/karust/openserp) [![CI](https://github.com/karust/openserp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/karust/openserp/actions/workflows/ci.yml) **OpenSERP** is a free, open-source API and CLI for accessing normalized search engine results from **Google, Yandex, Baidu, Bing, DuckDuckGo, and Ecosia**. @@ -38,10 +38,10 @@ Run it locally, self-host it, or use the optional hosted API when you do not wan ```bash # Run the API server via prebuilt image -docker run -p 127.0.0.1:7000:7000 -it karust/openserp serve -a 0.0.0.0 -p 7000 +docker run --rm -p 127.0.0.1:7000:7000 karust/openserp:latest serve -a 0.0.0.0 -p 7000 -# Or use docker-compose -docker compose up --build +# Or use docker-compose (pulls the prebuilt image) +docker compose up ``` ### From Source diff --git a/docker-compose.yaml b/docker-compose.yaml index 6b61056..6febeff 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,13 +1,14 @@ -version: '3' - services: openserp: container_name: serp init: true # Chrome crashes with "Out of memory" on page loads when /dev/shm is the default 64MB. Prefer an enlarged shm over `ipc:host` to keep the container isolated from the host IPC namespace. shm_size: 2gb - build: - context: . + image: karust/openserp:latest + # To build from source instead of pulling the prebuilt image, + # uncomment and run `docker compose up --build`: + #build: + # context: . ports: - 7000:7000 command: serve -l diff --git a/examples/README.md b/examples/README.md index 8f94ee5..e9ab66b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -32,12 +32,12 @@ curl "http://localhost:7000/mega/search?text=open+source+search+api&engines=bing ## SDKs and integrations -| Tool | Package | Install | -| --- | --- | --- | -| JavaScript / TypeScript | [`@openserp/sdk`](https://www.npmjs.com/package/@openserp/sdk) | `npm install @openserp/sdk` | -| Python | [`openserp`](https://pypi.org/project/openserp/) | `pip install openserp` | -| MCP server (AI agents) | [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp) | `npx @openserp/mcp` | -| n8n community node | [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp) | Install via n8n community nodes | +| Tool | Package | Install | +| ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------- | +| JavaScript / TypeScript | [`@openserp/sdk`](https://www.npmjs.com/package/@openserp/sdk) | `npm install @openserp/sdk` | +| Python | [`openserp`](https://pypi.org/project/openserp/) | `pip install openserp` | +| MCP server (AI agents) | [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp) | `npx @openserp/mcp` | +| n8n community node | [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp) | Install via n8n community nodes | ## Examples by question