diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de911a04..83d603b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,3 +90,74 @@ jobs: files: dist/prosper202-*.zip generate_release_notes: true fail_on_unmatched_files: true + + # Publish the production container image (the Dockerfile's default `app` + # stage) to GitHub Container Registry. This is what image-based deployments + # — including the Coolify service template in build/coolify-service-template/ + # — pull instead of building from a git checkout. Uses the built-in + # GITHUB_TOKEN; no external registry account. NOTE: the ghcr.io package is + # private after the very first push — flip it to public once in the package's + # settings on GitHub so anonymous `docker pull` works. + docker-image: + runs-on: ubuntu-latest + # Publish only after the release job succeeds: its tag-vs-version.php check + # and artifact build are the release validation — without this dependency a + # bad tag could still ship an image (and move `latest`) with no + # corresponding GitHub Release. + needs: release + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU (arm64 emulation) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: startsWith(github.ref, 'refs/tags/v') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # The versioned image tag comes straight from the git tag (which the + # release job this job depends on has already validated against + # version.php) rather than a semver parser: version.php's format permits + # values strict SemVer rejects (e.g. leading zeros), and a parser would + # silently skip the versioned tag for those. + - name: Derive version from tag + id: version + if: startsWith(github.ref, 'refs/tags/v') + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Compute image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=${{ steps.version.outputs.version }},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + # `latest` only for stable versions: version.php permits suffixed + # prereleases (e.g. v2.0.0-beta), and those must not replace the + # production image the Coolify template pulls by `:latest`. + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') }} + # Non-tag (workflow_dispatch) runs build without pushing; give the + # local build a deterministic name so the tag list is never empty. + type=sha,enable=${{ !startsWith(github.ref, 'refs/tags/v') }} + + # A workflow_dispatch run from a branch builds without pushing, so the + # image build stays verifiable outside the release flow. + - name: Build (and push on tags) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ startsWith(github.ref, 'refs/tags/v') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/build/README.md b/build/README.md index 8ab3839c..92407ab4 100644 --- a/build/README.md +++ b/build/README.md @@ -66,6 +66,7 @@ on first boot, so the setup wizard opens with the database step already done. | `test-install-config.php` | Pre-baked `202-config.php` mounted into the **test-install** stack (`docker-compose.test-install.yml`). Points at the `db-test` service. Credentials (`root` / `root_password`) are throwaway container defaults reachable only inside the compose network — not secrets. Committed on purpose so the test stack is one command. | | `staging-config.php` | Same idea for the **staging** stack (`docker-compose.staging.yml`), pointing at the `db2` service. *Gitignored* — `cp build/staging-config.sample.php build/staging-config.php` before bringing the staging stack up. | | `staging-config.sample.php` | Committed template for the above; defaults already match the staging compose stack, so the copy works as-is. | +| `coolify-service-template/` | Ready-to-submit kit for listing Prosper202 in Coolify's one-click service catalog: the image-based service template, logo SVG, and submission checklist (see its README). Derived from `../docker-compose.coolify.yaml` — keep the two in sync. | | `apache/my-tracking-proxy.conf` | Example **host-side** Apache reverse-proxy vhost (port 80/443 → the app container) so you can hit a real hostname like `my.tracking202.com` with TLS during local dev. Not part of any compose file; drop it into a host Apache and adjust the `ProxyPass` target port to whatever you bound the app to. | --- diff --git a/build/coolify-service-template/README.md b/build/coolify-service-template/README.md new file mode 100644 index 00000000..0346ad3d --- /dev/null +++ b/build/coolify-service-template/README.md @@ -0,0 +1,57 @@ +# Coolify one-click service template (submission kit) + +Everything needed to get Prosper202 listed in Coolify's one-click service +catalog (), prepared and ready to submit. The +catalog is generated from the [`coollabsio/coolify`](https://github.com/coollabsio/coolify) +repository — listing means getting a PR merged there. + +## Contents + +| File | Submits to (in coollabsio/coolify) | +|------|-------------------------------------| +| `prosper202.yaml` | `templates/compose/prosper202.yaml` | +| `svgs/prosper202.svg` | `svgs/prosper202.svg` | + +The template mirrors the repo's `docker-compose.coolify.yaml`, except it pulls +the published image (`ghcr.io/tracking202/prosper202`) instead of building +from git — one-click services never clone the repository. + +## Prerequisites (in order) + +1. **A published container image.** The release workflow + (`.github/workflows/release.yml`, `docker-image` job) publishes + `ghcr.io/tracking202/prosper202:{version,latest}` (amd64 + arm64) on every + `vX.Y.Z` tag using the built-in `GITHUB_TOKEN` — no registry account + needed. **One manual step after the first tagged release:** the GHCR + package is created private; open the package's settings on GitHub and set + it to **Public** so anonymous `docker pull` works. +2. **Coolify's star requirement.** The contribution guide + () asks that the + service's repository have **at least 1,000 GitHub stars**. Until the repo + crosses that bar, either open a GitHub Discussion with the Coolify + maintainers making the case for an exception, or hold the submission. + Everything else here stays valid either way. + +## Submission checklist + +1. Fork `coollabsio/coolify` on GitHub. +2. Copy `prosper202.yaml` to `templates/compose/prosper202.yaml` and + `svgs/prosper202.svg` to `svgs/prosper202.svg`. (Replace the placeholder + SVG with official vector brand art if available — the metadata header's + `logo:` line already points at the right path.) +3. Regenerate/reference the parsed templates per their contribution guide + (`templates/service-templates.json`). +4. **Test first**: on any Coolify instance, create a resource via + **Docker Compose Empty**, paste `prosper202.yaml` (without the metadata + comments), set a domain on the `prosper202` service, deploy, and complete + the setup wizard. +5. Open the PR. After it merges, add the docs page + (`/docs/services/prosper202.md` in their docs) so the service appears on + the website listing. + +## Keeping the template in sync + +`prosper202.yaml` is derived from `../../docker-compose.coolify.yaml`. If that +stack changes (new env vars, volumes, services), update this template in the +same commit — and after the catalog listing exists, upstream the same change +to `coollabsio/coolify`. diff --git a/build/coolify-service-template/prosper202.yaml b/build/coolify-service-template/prosper202.yaml new file mode 100644 index 00000000..cb9d92e1 --- /dev/null +++ b/build/coolify-service-template/prosper202.yaml @@ -0,0 +1,144 @@ +# documentation: https://prosper202.com +# slogan: Self-hosted PPC and affiliate conversion tracking platform (ClickServer). +# category: analytics +# tags: tracking, affiliate, ppc, analytics, marketing, conversion, clickserver +# logo: svgs/prosper202.svg +# port: 80 + +# Ready-to-submit Coolify one-click service template for Prosper202. +# Submission target: templates/compose/prosper202.yaml in coollabsio/coolify +# (see the README next to this file for the submission checklist). +# +# Mirrors the repo's docker-compose.coolify.yaml, except it pulls the +# published image instead of building from a git checkout — one-click +# services never clone the repository. The container self-writes its +# database config from the environment on boot, so the setup wizard opens +# with the database step already completed. + +services: + prosper202: + image: ghcr.io/tracking202/prosper202:latest + environment: + # FQDN routes the assigned domain to this service on port 80; URL is the + # same domain with the scheme, declared so Coolify generates it — the + # worker consumes it as P202_PUBLIC_ORIGIN for webhook download links. + - SERVICE_FQDN_PROSPER202_80 + - SERVICE_URL_PROSPER202 + - APP_ENV=production + # In-app 1-click upgrade writes to the ephemeral container filesystem; + # upgrades arrive as new image tags instead. + - P202_DISABLE_AUTO_UPGRADE=1 + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?} + - MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202} + - DB_HOST=db + - MC_HOST=memcached + # REST API v3 job/idempotency state — persisted outside the docroot. + - P202_SERVER_STATE_DIR=/var/lib/prosper202/api-v3-state + # Uploaded MaxMind ISP databases — persisted outside the docroot. + - P202_GEO_DIR=/var/lib/prosper202/geo + volumes: + - prosper202-exports:/var/www/html/202-config/temp/attribution-exports + - prosper202-api-state:/var/lib/prosper202/api-v3-state + - prosper202-geo:/var/lib/prosper202/geo + depends_on: + db: + condition: service_healthy + memcached: + condition: service_started + healthcheck: + test: ["CMD", "php", "-r", "exit(@file_get_contents('http://127.0.0.1/health/') === false ? 1 : 0);"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 30s + + db: + image: mysql:8.0 + environment: + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?} + - MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202} + volumes: + - prosper202-mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p\"$${MYSQL_ROOT_PASSWORD}\""] + interval: 5s + timeout: 5s + retries: 24 + + memcached: + image: memcached:1.6-alpine + healthcheck: + test: ["CMD-SHELL", "nc -z 127.0.0.1 11211"] + interval: 10s + timeout: 5s + retries: 6 + + # Polls the scheduled-jobs endpoint once a minute (it has its own lock and + # frequency tiers), replacing a host crontab. + cron: + image: alpine:3.20 + restart: unless-stopped + depends_on: + prosper202: + condition: service_started + command: > + sh -c 'while true; do + wget -q -O /dev/null http://prosper202/202-cronjobs/index.php || true; + sleep 60; + done' + healthcheck: + test: ["CMD", "true"] + interval: 30s + + # Standalone CLI workers the HTTP cron endpoint does not cover: attribution + # export queue and API v3 sync worker every minute, snapshot rebuild hourly. + # Runs the same image as the web service, as the web user, sharing the + # export/state volumes. + worker: + image: ghcr.io/tracking202/prosper202:latest + restart: unless-stopped + user: www-data + environment: + - APP_ENV=production + - P202_DISABLE_AUTO_UPGRADE=1 + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?} + - MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202} + - DB_HOST=db + - MC_HOST=memcached + - P202_SERVER_STATE_DIR=/var/lib/prosper202/api-v3-state + - P202_GEO_DIR=/var/lib/prosper202/geo + # Public origin for webhook download links (no request context in CLI). + - P202_PUBLIC_ORIGIN=${SERVICE_URL_PROSPER202:-} + volumes: + - prosper202-exports:/var/www/html/202-config/temp/attribution-exports + - prosper202-api-state:/var/lib/prosper202/api-v3-state + - prosper202-geo:/var/lib/prosper202/geo + depends_on: + db: + condition: service_healthy + memcached: + condition: service_started + command: + - bash + - -c + - | + last_rebuild=0 + while true; do + php 202-cronjobs/attribution-export.php || true + php 202-cronjobs/sync-worker.php || true + now=$$(date +%s) + if [ $$((now - last_rebuild)) -ge 3600 ]; then + php 202-cronjobs/attribution-rebuild.php || true + last_rebuild=$$now + fi + sleep 60 + done + healthcheck: + test: ["CMD", "true"] + interval: 30s + +volumes: + prosper202-mysql-data: + prosper202-exports: + prosper202-api-state: + prosper202-geo: diff --git a/build/coolify-service-template/svgs/prosper202.svg b/build/coolify-service-template/svgs/prosper202.svg new file mode 100644 index 00000000..efe4254b --- /dev/null +++ b/build/coolify-service-template/svgs/prosper202.svg @@ -0,0 +1,13 @@ + + + + + 202 + PROSPER +