diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc1c878 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI + +on: + push: + branches: [main, master] + tags: ["v*.*.*"] + pull_request: + branches: [main, master] + +# Required to publish to GHCR +permissions: + contents: read + packages: write + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build dependencies (ZeroMQ for CGO) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends pkg-config libzmq3-dev + + - name: Determine module directory + id: paths + shell: bash + run: | + if [[ -f go.mod ]]; then + echo "mod_dir=." >> "$GITHUB_OUTPUT" + elif [[ -f indexer/go.mod ]]; then + echo "mod_dir=indexer" >> "$GITHUB_OUTPUT" + else + echo "No go.mod found at repo root or indexer/" >&2 + exit 1 + fi + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: ${{ steps.paths.outputs.mod_dir }}/go.mod + cache: true + + - name: Download modules + working-directory: ${{ steps.paths.outputs.mod_dir }} + run: go mod download + + - name: Build + working-directory: ${{ steps.paths.outputs.mod_dir }} + env: + CGO_ENABLED: "1" # required by mattn/go-sqlite3 + run: go build -v ./... + + - name: Test + working-directory: ${{ steps.paths.outputs.mod_dir }} + env: + CGO_ENABLED: "1" + run: go test -v ./... + + docker: + if: startsWith(github.ref, 'refs/tags/v') + name: Build and Push Docker image + needs: build-and-test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine module directory + id: paths + shell: bash + run: | + if [[ -f go.mod ]]; then + echo "mod_dir=." >> "$GITHUB_OUTPUT" + elif [[ -f indexer/go.mod ]]; then + echo "mod_dir=indexer" >> "$GITHUB_OUTPUT" + else + echo "No go.mod found at repo root or indexer/" >&2 + exit 1 + fi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + build-args: | + MODULE_DIR=${{ steps.paths.outputs.mod_dir }} + push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..abc45f4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.21-bookworm AS build +ARG MODULE_DIR=. +WORKDIR /src +# Install build dependencies for CGO (ZeroMQ, pkg-config, toolchain) +RUN apt-get update && apt-get install -y --no-install-recommends build-essential pkg-config libzmq3-dev && rm -rf /var/lib/apt/lists/* +# Copy module files first for better caching +COPY ${MODULE_DIR}/go.mod ${MODULE_DIR}/go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download +# Copy source +COPY ${MODULE_DIR}/ ./ +# Build with CGO enabled (needed for sqlite3 and zmq4) +ENV CGO_ENABLED=1 +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + go build -o /out/indexer ./main.go + +# Runtime image with libzmq5 available +FROM debian:bookworm-slim +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libzmq5 && rm -rf /var/lib/apt/lists/* +COPY --from=build /out/indexer /app/indexer +EXPOSE 8000 +ENTRYPOINT ["/app/indexer"] diff --git a/main.go b/main.go index eb04db0..986ccec 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,8 @@ import ( "flag" "fmt" "log" + "os" + "strconv" "time" "github.com/dogeorg/doge" @@ -37,17 +39,17 @@ func main() { log.Printf("\n\n[Indexer] starting") var config Config - flag.StringVar(&config.connStr, "dburl", "index.db", "Database connection string") - flag.StringVar(&config.rpcHost, "rpchost", "127.0.0.1", "RPC host") - flag.IntVar(&config.rpcPort, "rpcport", 22555, "RPC port") - flag.StringVar(&config.rpcUser, "rpcuser", "dogecoin", "RPC username") - flag.StringVar(&config.rpcPass, "rpcpass", "dogecoin", "RPC password") - flag.StringVar(&config.zmqHost, "zmqhost", "127.0.0.1", "ZMQ host") - flag.IntVar(&config.zmqPort, "zmqport", 28332, "ZMQ port") - flag.StringVar(&config.bindAPI, "bindapi", "localhost:8000", "API bind address") - flag.StringVar(&config.corsOrigin, "cors-origin", "http://localhost:5173", "CORS allowed origin") - flag.StringVar(&config.chainName, "chain", "mainnet", "Chain Params (mainnet, testnet, regtest)") - flag.Int64Var(&config.startingHeight, "startingheight", 5830000, "Starting Height") + flag.StringVar(&config.connStr, "dburl", getEnv("DB_URL", "index.db"), "Database connection string") + flag.StringVar(&config.rpcHost, "rpchost", getEnv("RPC_HOST", "127.0.0.1"), "RPC host") + flag.IntVar(&config.rpcPort, "rpcport", getEnvInt("RPC_PORT", 22555), "RPC port") + flag.StringVar(&config.rpcUser, "rpcuser", getEnv("RPC_USER", "dogecoin"), "RPC username") + flag.StringVar(&config.rpcPass, "rpcpass", getEnv("RPC_PASS", "dogecoin"), "RPC password") + flag.StringVar(&config.zmqHost, "zmqhost", getEnv("ZMQ_HOST", "127.0.0.1"), "ZMQ host") + flag.IntVar(&config.zmqPort, "zmqport", getEnvInt("ZMQ_PORT", 28332), "ZMQ port") + flag.StringVar(&config.bindAPI, "bindapi", getEnv("BIND_API", "localhost:8000"), "API bind address") + flag.StringVar(&config.corsOrigin, "cors-origin", getEnv("CORS_ORIGIN", "http://localhost:5173"), "CORS allowed origin") + flag.StringVar(&config.chainName, "chain", getEnv("CHAIN", "mainnet"), "Chain Params (mainnet, testnet, regtest)") + flag.Int64Var(&config.startingHeight, "startingheight", getEnvInt64("STARTING_HEIGHT", 5830000), "Starting Height") flag.Parse() @@ -121,3 +123,38 @@ func main() { gov.Start().WaitForShutdown() fmt.Println("[Indexer] stopped") } + +func getEnv(key, fallback string) string { + v := os.Getenv(key) + if v != "" { + return v + } + return fallback +} + +func getEnvInt(key string, fallback int) int { + if v := os.Getenv(key); v != "" { + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + return fallback +} + +func getEnvInt64(key string, fallback int64) int64 { + if v := os.Getenv(key); v != "" { + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + return i + } + } + return fallback +} + +func getEnvBool(key string, fallback bool) bool { + if v := os.Getenv(key); v != "" { + if b, err := strconv.ParseBool(v); err == nil { + return b + } + } + return fallback +}