{"id":34218013,"url":"https://github.com/psyb0t/servicepack","last_synced_at":"2026-04-01T18:56:32.325Z","repository":{"id":314199982,"uuid":"1054558146","full_name":"psyb0t/servicepack","owner":"psyb0t","description":"A Go framework for building concurrent service applications without the usual boilerplate bullshit.  Write once, deploy everywhere - run your entire stack locally for debugging or distribute services across machines with a single env var. No Docker Compose nightmares or managing 47 separate repos.","archived":false,"fork":false,"pushed_at":"2026-03-19T23:38:29.000Z","size":10592,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-20T11:28:33.908Z","etag":null,"topics":["boilerplate","boilerplate-template","debug-enhancement","framework","go","microservices","monolith","multi-service-cluster","parallel","service-oriented","single-binary-deployment","software-architecture","template"],"latest_commit_sha":null,"homepage":"https://ciprian.51k.eu/servicepack-the-go-framework-that-actually-understands-how-real-development-works/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/psyb0t.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-11T02:42:38.000Z","updated_at":"2026-03-19T23:38:30.000Z","dependencies_parsed_at":"2025-09-11T06:09:40.734Z","dependency_job_id":"098512ad-6309-4714-95df-ab854e4e77a0","html_url":"https://github.com/psyb0t/servicepack","commit_stats":null,"previous_names":["psyb0t/servicepack"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/psyb0t/servicepack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psyb0t%2Fservicepack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psyb0t%2Fservicepack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psyb0t%2Fservicepack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psyb0t%2Fservicepack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/psyb0t","download_url":"https://codeload.github.com/psyb0t/servicepack/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psyb0t%2Fservicepack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290980,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["boilerplate","boilerplate-template","debug-enhancement","framework","go","microservices","monolith","multi-service-cluster","parallel","service-oriented","single-binary-deployment","software-architecture","template"],"created_at":"2025-12-15T22:45:33.856Z","updated_at":"2026-04-01T18:56:32.316Z","avatar_url":"https://github.com/psyb0t.png","language":"Go","readme":"```\n___ ___ _____   _____ ___ ___ ___  _   ___ _  __\n/ __| __| _ \\ \\ / /_ _/ __| __| _ \\/_\\ / __| |/ /\n\\__ \\ _||   /\\ V / | | (__| _||  _/ _ \\ (__| ' \u003c\n|___/___|_|_\\ \\_/ |___\\___|___|_|/_/ \\_\\___|_|\\_\\\n```\n\n# servicepack\n\nA Go service framework that runs your shit concurrently without fucking around.\n\n## Table of Contents\n\n**Getting Started**\n\n- [What is this?](#what-is-this)\n- [Quick Start (Make It Your Own in 30 Seconds)](#quick-start-make-it-your-own-in-30-seconds)\n- [Just Want to Try It First?](#just-want-to-try-it-first)\n\n**Core Concepts**\n\n- [Creating Services](#creating-services)\n- [Service Interface](#service-interface)\n  - [Optional Interfaces](#optional-interfaces)\n  - [Custom CLI Commands](#custom-cli-commands)\n  - [Custom Initialization](#custom-initialization)\n  - [Lifecycle Hooks](#lifecycle-hooks)\n- [How Services Actually Work](#how-services-actually-work)\n  - [Service Filtering](#service-filtering)\n\n**Essential Tools**\n\n- [The Makefile (Your New Best Friend)](#the-makefile-your-new-best-friend)\n  - [Basic Commands](#basic-commands)\n  - [Service Management](#service-management)\n  - [Development](#development)\n  - [Framework Management](#framework-management)\n  - [Backup Management](#backup-management)\n  - [Script Customization](#script-customization)\n\n**System Details**\n\n- [Architecture](#architecture)\n  - [Key Components](#key-components)\n- [Environment Variables](#environment-variables)\n- [Build System Details](#build-system-details)\n  - [Build Process](#build-process)\n\n**Framework Management**\n\n- [Framework Updates](#framework-updates)\n  - [Review and Apply Updates](#review-and-apply-updates)\n  - [Customizing Updates with .servicepackupdateignore](#customizing-updates-with-servicepackupdateignore)\n\n**Internals**\n\n- [Concurrency Model](#concurrency-model)\n- [Error Handling](#error-handling)\n- [Testing](#testing)\n  - [Test Isolation](#test-isolation)\n- [Pre-commit Hook](#pre-commit-hook)\n\n**Reference**\n\n- [Dependencies](#dependencies)\n- [Directory Structure](#directory-structure)\n- [Example Services](#example-services)\n- [License](#license)\n\n## What is this?\n\nYou write services, this thing runs them. All your services go into one binary so you can debug the fuck out of service-to-service calls without dealing with distributed bullshit. Run everything locally, then deploy individual services as microservices when you're ready. Or just fuckin' deploy everything together, y not.\n\n## Quick Start (Make It Your Own in 30 Seconds)\n\n```bash\n# Clone this shit\ngit clone https://github.com/psyb0t/servicepack\ncd servicepack\n\n# Make it yours\nmake own MODNAME=github.com/yourname/yourproject\n\n# Build and run\nmake build\n./build/yourproject run\n```\n\nThis will:\n\n- Nuke the .git directory\n- Replace the module name everywhere\n- Set you up with a fresh go.mod\n- Replace README with just your project name\n- Run `git init` to start fresh\n- Setup dependencies\n- Create initial commit on main branch\n\nYou'll see the hello-world service spamming \"Hello, World!\" every 5 seconds. Hit Ctrl+C to stop it cleanly.\n\n## Just Want to Try It First?\n\n```bash\ngit clone https://github.com/psyb0t/servicepack\ncd servicepack\nmake run-dev\n```\n\nThis builds a dev Docker image and runs it with debug logging. You'll see all the example services in action - retries, dependencies, allowed failures, one-shot jobs, and a crasher that takes everything down after ~30 seconds.\n\n## Creating Services\n\nCreate a new service:\n\n```bash\nmake service NAME=my-cool-service\n```\n\nThis shits out a skeleton service at `internal/pkg/services/my-cool-service/`. Edit the generated file, put your logic in the `Run()` method. Done - your service starts automatically.\n\nRemove a service:\n\n```bash\nmake service-remove NAME=my-cool-service\n```\n\n## Service Interface\n\nEvery service implements this interface:\n\n```go\ntype Service interface {\n    Name() string                        // Return service name\n    Run(ctx context.Context) error      // Your service logic goes here\n    Stop(ctx context.Context) error     // Cleanup logic (optional)\n}\n```\n\nThe `Run()` method should:\n\n- Listen for `ctx.Done()` and return cleanly when cancelled\n- Return an error if something goes wrong (this will stop all services)\n- Do whatever the fuck your service is supposed to do\n\nThe `Stop()` method is for cleanup - it runs when the app is shutting down.\n\n### Optional Interfaces\n\nServices can opt into advanced behavior by implementing these:\n\n```go\n// Retryable - service gets restarted on failure\ntype Retryable interface {\n    MaxRetries() int\n    RetryDelay() time.Duration  // e.g. 5*time.Second\n}\n\n// AllowedFailure - service can die without killing everything\ntype AllowedFailure interface {\n    IsAllowedFailure() bool\n}\n\n// Dependent - service waits for other services to start first\ntype Dependent interface {\n    Dependencies() []string  // service names\n}\n\n// ReadyNotifier - signal when actually ready to serve\ntype ReadyNotifier interface {\n    Ready() \u003c-chan struct{}\n}\n\n// Commander - expose CLI subcommands\ntype Commander interface {\n    Commands() []*cobra.Command\n}\n```\n\n**Retry**: When `Run()` returns an error, the service manager retries up to `MaxRetries()` times with `RetryDelay()` between attempts. If context is cancelled during the delay, it bails cleanly.\n\n**Allowed Failure**: When a service fails (even after retries), its error gets logged but doesn't propagate - other services keep running. Perfect for non-critical shit like cache warmers or metrics exporters.\n\n**Dependencies**: The service manager resolves a dependency graph using topological sort. Services with no deps start first, then their dependents, etc. Cyclic dependencies are detected and rejected. Dependencies on services not in the current process (external databases, services on other servers) are skipped with a debug log.\n\n**Ready Notification**: Services that implement `ReadyNotifier` signal when they're actually ready (listening, connected, etc.). The service manager waits for the Ready channel before starting dependent services. Services without it are considered ready as soon as their goroutine launches.\n\n**CLI Commands**: Services that implement `Commander` get their own CLI namespace: `./app \u003cservicename\u003e \u003csubcommand\u003e`. Only that service gets instantiated - no other services are touched. Returns standard cobra commands so you get flags, args, help, everything for free.\n\nYou can combine them - a service can be retryable AND an allowed failure AND have dependencies AND signal readiness AND expose CLI commands.\n\n### Custom CLI Commands\n\n`cmd/commands.go` is your hook to add CLI commands. It's never touched by framework updates:\n\n```go\n// cmd/commands.go\npackage main\n\nimport \"github.com/spf13/cobra\"\n\nfunc commands() []*cobra.Command {\n    return []*cobra.Command{\n        {\n            Use:   \"seed\",\n            Short: \"Seed the database\",\n            Run: func(_ *cobra.Command, _ []string) {\n                // your logic\n            },\n        },\n    }\n}\n```\n\nThen `./app seed` just works. These are standalone commands separate from service commands.\n\n### Custom Initialization\n\n`cmd/init.go` is your hook to run shit before the app starts. It's never touched by framework updates. Use it to add custom slog handlers, set up global config, or anything else:\n\n```go\n// cmd/init.go\npackage main\n\nimport slogconfigurator \"github.com/psyb0t/slog-configurator\"\n\nfunc init() {\n    slogconfigurator.AddHandler(myLokiHandler)\n}\n```\n\nEvery `slog.Info/Error/etc` call across the entire app - framework, services, everything - goes to all registered handlers. Want Loki? Datadog? Elasticsearch? Just write a `slog.Handler` and plug it in here.\n\n### Lifecycle Hooks\n\nThe `App` exposes hooks so you can run custom logic at specific points in the lifecycle without touching framework files:\n\n```go\n// cmd/init.go\npackage main\n\nimport (\n    \"context\"\n\n    \"github.com/yourname/yourproject/internal/app\"\n    \"github.com/yourname/yourproject/internal/pkg/metrics\"\n)\n\nfunc init() {\n    // Runs before any service starts — use it to launch background\n    // goroutines, set up metrics pushers, warm caches, etc.\n    app.GetInstance().OnPreRun(func(ctx context.Context) {\n        go metrics.StartPush(ctx, \"myapp\")\n    })\n\n    // Runs after all services have stopped — use it for final\n    // cleanup, flushing buffers, closing connections, etc.\n    app.GetInstance().OnPostStop(func(ctx context.Context) {\n        metrics.Flush()\n    })\n}\n```\n\nHooks execute sequentially in registration order. Pre-run hooks receive the app context so spawned goroutines respect the app lifecycle. Multiple hooks can be registered — they all run.\n\n## How Services Actually Work\n\n1. Services are auto-discovered using the [`gofindimpl`](https://github.com/psyb0t/gofindimpl) tool\n2. The `scripts/make/service_registration.sh` script finds all Service implementations\n3. It generates `internal/pkg/services/services.gen.go` with a `services.Init()` function\n4. `services.Init()` registers service factories (cheap, no connections) at startup\n5. Factories are only called when actually needed: `./app run` instantiates all (filtered by `SERVICES_ENABLED`), `./app \u003cservice\u003e \u003csubcommand\u003e` instantiates only that service\n\n### Service Filtering\n\nBy default, all services run. To run specific services:\n\n```bash\nexport SERVICES_ENABLED=\"hello-world,my-cool-service\"\n./build/servicepack run\n```\n\nLeave `SERVICES_ENABLED` empty or unset to run all services.\n\n## The Makefile (Your New Best Friend)\n\n### Basic Commands\n\n- `make all` - Full pipeline: dep → lint-fix → test-coverage → build\n- `make build` - Build the binary using Docker (static linking)\n- `make dep` - Get dependencies with `go mod tidy` and `go mod vendor`\n- `make test` - Run all tests with race detection\n- `make test-coverage` - Run tests with 90% coverage requirement (excludes example services and cmd packages)\n- `make lint` - Lint with `go fix` (diff-only) + golangci-lint (80+ linters)\n- `make lint-fix` - Apply `go fix` modernizations + golangci-lint auto-fixes\n- `make clean` - Clean build artifacts and coverage files\n\n### Service Management\n\n- `make service NAME=foo` - Create new service\n- `make service-remove NAME=foo` - Remove service\n- `make service-registration` - Regenerate service discovery\n\n### Development\n\n- `make run-dev` - Run in development Docker container\n- `make docker-build-dev` - Build dev image\n\n### Docker\n\n- `make docker-build` - Build production Docker image\n- `make docker-build-dev` - Build development Docker image\n\n### Framework Management\n\n- `make servicepack-update` - Update to latest servicepack framework (creates backup first)\n- `make servicepack-update-review` - Review pending framework update changes\n- `make servicepack-update-merge` - Merge pending framework update\n- `make servicepack-update-revert` - Revert pending framework update\n- `make own MODNAME=github.com/you/project` - Make this framework your own\n\n### Backup Management\n\n- `make backup` - Create timestamped backup in `/tmp` and `.backup/`\n- `make backup-restore [BACKUP=filename.tar.gz]` - Restore from backup (defaults to latest, nukes everything first)\n- `make backup-clear` - Delete all backup files\n\n**Note**: Framework updates (`make servicepack-update`) automatically create backups before making changes.\n\n### Script Customization\n\nYou can override any framework script by creating a user version:\n\n```bash\n# Create custom script (will override framework version)\ncp scripts/make/servicepack/test.sh scripts/make/test.sh\n# Edit your custom version\nvim scripts/make/test.sh\n```\n\nThe Makefile checks for user scripts first (`scripts/make/`), then falls back to framework scripts (`scripts/make/servicepack/`). This lets you customize any build step while preserving the ability to update the framework without conflicts.\n\n**Framework scripts** (in `scripts/make/servicepack/`):\n\n- Get updated when you run `make servicepack-update`\n- Always preserved - your customizations won't get overwritten\n\n**User scripts** (in `scripts/make/`):\n\n- Take priority over framework scripts\n- Never touched by framework updates\n- Perfect for project-specific build customizations\n\n### Makefile Customization\n\nThe build system uses a split Makefile approach:\n\n```bash\n# Override any framework command by defining it in your Makefile\nbuild: ## Custom build command\n\t@echo \"Running my custom build...\"\n\t@docker build -t myapp .\n\n# Add your own custom commands\ndeploy: ## Deploy to production\n\t@./deploy.sh\n```\n\n**How it works:**\n\n- `Makefile.servicepack` - Contains all framework commands (updated by framework)\n- `Makefile` - Your file that includes servicepack + allows custom commands (never touched)\n- User commands override framework commands automatically\n- `make help` shows both user and framework commands\n\n### Dockerfile Customization\n\nBoth development and production Docker environments use the override pattern:\n\n```bash\n# Customize development environment\ncp Dockerfile.servicepack.dev Dockerfile.dev\nvim Dockerfile.dev\n\n# Customize production environment\ncp Dockerfile.servicepack Dockerfile\nvim Dockerfile\n```\n\n**How it works:**\n\n- `Dockerfile.servicepack.dev` - Framework development image (updated by framework)\n- `Dockerfile.dev` - Your custom development image (never touched)\n- `Dockerfile.servicepack` - Framework production image (updated by framework)\n- `Dockerfile` - Your custom production image (never touched)\n- `make docker-build-dev` automatically uses your custom development version if it exists\n\n## Architecture\n\n```\ncmd/main.go                          # Entry point, CLI setup\ninternal/app/                        # Application layer\n├── app.go                          # Main app orchestration\ninternal/pkg/\n├── service-manager/                 # Framework service orchestration\n│   ├── service_manager.go          # Concurrent service runner\n│   ├── errors.go                   # Framework error definitions\n│   └── *_test.go                   # Framework tests\n└── services/                       # User service space\n    ├── services.gen.go             # Auto-generated services.Init() function\n    ├── hello-world/                # Example: basic long-running service\n    ├── example-database/           # Example: retryable service\n    ├── example-api/                # Example: service with dependencies\n    ├── example-migrator/           # Example: one-shot with allowed failure\n    ├── example-optional/           # Example: allowed failure\n    ├── example-flaky/              # Example: fails then recovers\n    ├── example-crasher/            # Example: crashes and kills everything\n    └── your-service/               # Your services go here\nscripts/make/                        # Build script system\n├── servicepack/                    # Framework scripts (updated by framework)\n│   ├── build.sh                   # Docker build script\n│   ├── dep.sh                     # Dependency management\n│   ├── test.sh                    # Test runner\n│   └── *.sh                       # Other framework scripts\n└── [custom scripts]               # User overrides (take priority)\n```\n\n### Key Components\n\n**ServiceManager**: Runs your services concurrently with dependency ordering, automatic retries, and allowed failures. Handles shutdown and error propagation. It's a singleton because globals are fine when you know what you're doing.\n\n**Service Registration**: Auto-discovery using [`gofindimpl`](https://github.com/psyb0t/gofindimpl) finds all your Service implementations and generates a `services.Init()` function that registers factories. No manual registration bullshit. Services are only instantiated when needed - `run` creates all, CLI commands create only the one they need.\n\n**App**: Wrapper that runs the ServiceManager and handles the lifecycle shit. Exposes `OnPreRun` and `OnPostStop` hooks so downstream projects can inject custom logic without modifying framework files.\n\n## Environment Variables\n\nThe framework uses these:\n\n```bash\n# Logging (via slog-configurator)\nLOG_LEVEL=debug          # debug, info, warn, error\nLOG_FORMAT=json          # json, text\nLOG_ADD_SOURCE=true      # show file:line in logs\n\n# Environment (via goenv)\nENV=dev                  # dev, prod (default: prod)\n\n# Runner\nRUNNER_SHUTDOWNTIMEOUT=10s   # graceful shutdown timeout (default: 10s)\n\n# Service filtering\nSERVICES_ENABLED=service1,service2   # comma-separated, empty = all\n\n# Your services can define their own env vars\n```\n\n## Build System Details\n\nThe build system is dynamic as fuck:\n\n1. App name is extracted from `go.mod` automatically\n2. Binary gets built with static linking (no external deps)\n3. App name is injected at build time via ldflags\n4. Docker builds ensure consistent environment\n\n### Build Process\n\n```makefile\nAPP_NAME := $(shell head -n 1 go.mod | awk '{print $2}' | awk -F'/' '{print $NF}')\n\nbuild:\n    docker run --rm -v $(PWD):/app -w /app golang:1.26-alpine \\\n        sh -c \"apk add --no-cache gcc musl-dev \u0026\u0026 \\\n               CGO_ENABLED=0 go build -a \\\n               -ldflags '-extldflags \\\"-static\\\" -X main.appName=$(APP_NAME)' \\\n               -o ./build/$(APP_NAME) ./cmd/...\"\n```\n\nThis means your binary name matches your module name automatically.\n\n## Framework Updates\n\nKeep your servicepack framework up to date:\n\n```bash\nmake servicepack-update\n```\n\nThis script:\n\n1. Checks for uncommitted changes (fails if found)\n2. Compares current version with latest\n3. Creates backup if update is needed\n4. Creates update branch `servicepack_update_to_VERSION`\n5. Downloads latest framework and applies changes\n6. Commits changes to update branch for review\n7. Leaves you on update branch to review and test\n\n### Review and Apply Updates\n\nAfter running `make servicepack-update`:\n\n```bash\n# Review what changed\nmake servicepack-update-review\n\n# Test the update\nmake dep \u0026\u0026 make service-registration \u0026\u0026 make test\n\n# If satisfied, merge the update\nmake servicepack-update-merge\n\n# If not satisfied, discard the update\nmake servicepack-update-revert\n```\n\n### Customizing Updates with .servicepackupdateignore\n\nCreate a `.servicepackupdateignore` file to exclude files from framework updates:\n\n```\n# Custom framework modifications (these are already user files)\n# Note: Dockerfile, Dockerfile.dev, Makefile, and scripts/make/ are automatically excluded\n\n# Local configuration files\n*.local\n.env*\n```\n\n**Framework vs User Files**:\n\n```\ncmd/                           # Framework files\ninternal/app/                  # Framework files\ninternal/pkg/service-manager/  # Framework files\nscripts/make/servicepack/      # Framework scripts (updated by servicepack-update)\nscripts/make/                  # User scripts (override framework, never touched)\nMakefile.servicepack           # Framework Makefile (updated by servicepack-update)\nMakefile                       # User Makefile (includes servicepack, never touched)\nDockerfile.servicepack.dev     # Framework development image (updated by servicepack-update)\nDockerfile.dev                 # User development image (overrides framework, never touched)\nDockerfile.servicepack         # Framework production image (updated by servicepack-update)\nDockerfile                     # User production image (never touched)\n.github/                       # Framework files (CI/CD workflows)\nLICENSE                        # Your project license\n.golangci.yml                  # Framework files\ngo.mod                         # Your module name preserved\ngo.sum                         # Gets regenerated\nREADME.md                      # Your project docs\ninternal/pkg/services/         # Your services - never touched\n```\n\nUse `.servicepackupdateignore` to exclude any framework files you've customized.\n\n## Pre-commit Hook\n\nThere's a `pre-commit.sh` script that runs `make lint \u0026\u0026 make test-coverage`. You can:\n\n- Use your favorite pre-commit tool to manage hooks\n- Use [`ez-pre-commit`](https://github.com/psyb0t/ez-pre-commit) to auto-setup Git hooks that run this script\n- Just use the simple script as-is (it runs lint and coverage checks)\n\n## Testing\n\nTests are structured per component:\n\n- `internal/app/app_test.go` - Application tests with mock services\n- `internal/pkg/service-manager/service_manager_test.go` - Unit tests for retry, allowed failure, dependencies, concurrency\n- `internal/pkg/service-manager/service_manager_integration_test.go` - Integration tests combining all features end-to-end\n- `internal/pkg/service-manager/errors_test.go` - Error definition and matching tests\n- Each service should have its own `*_test.go` files\n\n90% test coverage is required by default (excludes example services and cmd packages). The coverage check runs with race detection and fails if below threshold.\n\n### Test Isolation\n\n- `ResetInstance()` resets the singleton for clean test state\n- `ClearServices()` clears all registered services\n- Mock services implement the Service interface for testing\n- Tests should avoid calling `services.Init()` and manually add mock services instead\n\n## Concurrency Model\n\n- Each service runs in its own goroutine\n- ServiceManager uses sync.WaitGroup for coordination\n- Context cancellation for clean shutdown\n- Services are started in dependency order (topological sort)\n- Services in the same dependency group start concurrently\n- Retryable services get restarted automatically on failure\n- Allowed-failure services can die without killing everything\n- Graceful shutdown cancels context and calls Stop() in reverse dependency order\n- Per-service stop timeout (30s default) prevents hung shutdowns\n- Panics in services are recovered and treated as errors\n\n## Error Handling\n\n- Service errors bubble up through the ServiceManager\n- First non-allowed-failure error stops all services\n- Allowed failures are logged but don't propagate\n- Retryable services exhaust retries before propagating\n- Context errors (cancellation) are treated as clean shutdown\n- All errors use [`ctxerrors`](https://github.com/psyb0t/ctxerrors) for context preservation\n- `ErrNoEnabledServices` - no services registered\n- `ErrCyclicDependency` - circular dependency detected\n- `ErrServicePanic` - service panicked (recovered, treated as error)\n- `ErrStopTimeout` - service didn't stop within timeout\n\n## Dependencies\n\nCore dependencies:\n\n- `log/slog` with [`slog-configurator`](https://github.com/psyb0t/slog-configurator) - Logging\n- [`github.com/spf13/cobra`](https://github.com/spf13/cobra) - CLI\n- [`github.com/psyb0t/ctxerrors`](https://github.com/psyb0t/ctxerrors) - Error handling\n- [`github.com/psyb0t/goenv`](https://github.com/psyb0t/goenv) - Environment detection (prod/dev)\n\nDevelopment dependencies:\n\n- [`golangci-lint`](https://github.com/golangci/golangci-lint) - Comprehensive linting (80+ linters: errcheck, govet, staticcheck, gosec, etc.)\n- [`testify`](https://github.com/stretchr/testify) - Testing assertions and mocks\n- [`gofindimpl`](https://github.com/psyb0t/gofindimpl) - Service auto-discovery tool\n\n## Directory Structure\n\n```\n.\n├── cmd/\n│   ├── main.go                    # Entry point\n│   └── init.go                    # Your custom init hooks\n├── pkg/runner/                     # App lifecycle runner\n├── internal/\n│   ├── app/                        # Application layer\n│   └── pkg/services/               # Services\n├── scripts/make/                   # Build script system\n│   ├── servicepack/               # Framework scripts (auto-updated)\n│   └── [user scripts]             # User overrides (take priority)\n├── build/                          # Build output\n├── vendor/                         # Vendored dependencies\n├── Makefile                        # User Makefile (includes servicepack framework)\n├── Makefile.servicepack            # Framework Makefile (auto-updated)\n├── Dockerfile                      # User production image (optional override)\n├── Dockerfile.dev                  # User development image (optional override)\n├── Dockerfile.servicepack          # Framework production image (auto-updated)\n├── Dockerfile.servicepack.dev      # Framework development image (auto-updated)\n└── servicepack.version             # Framework version tracking\n```\n\n## Example Services\n\nThe framework ships with example services that demonstrate every lifecycle pattern:\n\n| Service | Pattern |\n|---|---|\n| `hello-world` | Long-running, no deps, basic service |\n| `example-database` | Long-running, retryable (2 retries, 2s delay), signals ready after startup |\n| `example-api` | Long-running, depends on database + flaky |\n| `example-migrator` | One-shot, depends on database, allowed failure, CLI commands (up/down/status) |\n| `example-optional` | Allowed failure, fails immediately but app keeps running |\n| `example-flaky` | Retryable (2 retries, 1s delay), fails twice then recovers |\n| `example-crasher` | Retryable (2 retries, 3s delay), fails all retries and kills everything |\n| `example-nested/http` | Nested directory, shares package name `server` with grpc sibling |\n| `example-nested/grpc` | Nested directory, shares package name `server` with http sibling |\n\nServices can live in nested directories under `internal/pkg/services/`. The codegen derives unique import aliases from the directory path, so `example-nested/http` and `example-nested/grpc` both use `package server` but get aliases `examplenestedhttp` and `examplenestedgrpc` - no collisions.\n\nRun them all: `go run ./cmd run` or `make run-dev`\n\nThese get removed when you run `make own`.\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsyb0t%2Fservicepack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpsyb0t%2Fservicepack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsyb0t%2Fservicepack/lists"}