From 940ae004b11f2b6350b7dbc3a14408165ca6c615 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 22:06:37 +0000 Subject: [PATCH 1/3] Publish container image on release; add Coolify catalog submission kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groundwork for listing Prosper202 in Coolify's one-click service catalog (coolify.io/services), which is generated from the coollabsio/coolify repository: - release.yml gains a docker-image job: on every vX.Y.Z tag it publishes the Dockerfile's default app stage to ghcr.io/tracking202/prosper202:{version,latest} for linux/amd64 and linux/arm64, using the built-in GITHUB_TOKEN (no external registry account). workflow_dispatch runs build without pushing. One manual step remains after the first tagged release: flip the GHCR package to public in its settings so anonymous docker pull works. - build/coolify-service-template/ holds the ready-to-submit kit: the catalog template (image-based mirror of docker-compose.coolify.yaml with the required metadata header — one-click services pull a published image, never build from git), a brand-colored placeholder logo SVG, and a README with the submission checklist. Blocking caveat documented there: Coolify's contribution guide requires the service repo to have at least 1,000 GitHub stars (currently 22), so the submission waits on either star growth or a maintainer exception; the image publishing is valuable independently. Template validated with docker compose config (5 services, 4 named volumes); workflow and SVG parse clean. The multi-arch image build itself runs first in CI (workflow_dispatch) — not buildable in this sandbox because composer's downloads are blocked by egress policy. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SJt817AkfBdUYoXnvaN8vh --- .github/workflows/release.yml | 50 +++++++ build/README.md | 1 + build/coolify-service-template/README.md | 57 +++++++ .../coolify-service-template/prosper202.yaml | 140 ++++++++++++++++++ .../svgs/prosper202.svg | 13 ++ 5 files changed, 261 insertions(+) create mode 100644 build/coolify-service-template/README.md create mode 100644 build/coolify-service-template/prosper202.yaml create mode 100644 build/coolify-service-template/svgs/prosper202.svg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de911a04..e3362cf7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,3 +90,53 @@ 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 + 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 }} + + - name: Compute image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=raw,value=latest,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..0dc1e688 --- /dev/null +++ b/build/coolify-service-template/prosper202.yaml @@ -0,0 +1,140 @@ +# 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: + - SERVICE_FQDN_PROSPER202_80 + - 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 + From c3a4c32b9d26f90f638fb2d17e1287f1e64cb055 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 01:34:40 +0000 Subject: [PATCH 2/3] Address PR review: gate image on release, guard latest and variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five verified findings from the bot reviews on tracking202/prosper202#145: - docker-image now needs: release, so a tag that fails the tag-vs-version.php check or the artifact build can no longer publish an image (or move `latest`) with no corresponding GitHub Release. - `latest` is only emitted for stable versions: version.php permits suffixed prereleases (v2.0.0-beta), which must not replace the production image the catalog template pulls by `:latest`. - The catalog template now declares SERVICE_URL_PROSPER202 on the routed service (Coolify generates FQDN and URL variants independently), matching what docker-compose.coolify.yaml already does — without it the worker's P202_PUBLIC_ORIGIN would always be empty on catalog deployments. - ${SERVICE_PASSWORD_MYSQL:?} fails fast at all three sites instead of silently substituting an empty password, and the worker's ${SERVICE_URL_PROSPER202:-} matches the main stack's pattern so plain-compose validation stays clean. Verified: workflow YAML parses; the template passes docker compose config with the password set and errors loudly without it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SJt817AkfBdUYoXnvaN8vh --- .github/workflows/release.yml | 10 +++++++++- build/coolify-service-template/prosper202.yaml | 12 ++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3362cf7..24c54f64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,6 +100,11 @@ jobs: # 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 @@ -128,7 +133,10 @@ jobs: images: ghcr.io/${{ github.repository }} tags: | type=semver,pattern={{version}} - type=raw,value=latest,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, '-') }} # A workflow_dispatch run from a branch builds without pushing, so the # image build stays verifiable outside the release flow. diff --git a/build/coolify-service-template/prosper202.yaml b/build/coolify-service-template/prosper202.yaml index 0dc1e688..cb9d92e1 100644 --- a/build/coolify-service-template/prosper202.yaml +++ b/build/coolify-service-template/prosper202.yaml @@ -19,12 +19,16 @@ 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_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?} - MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202} - DB_HOST=db - MC_HOST=memcached @@ -51,7 +55,7 @@ services: db: image: mysql:8.0 environment: - - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL} + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?} - MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202} volumes: - prosper202-mysql-data:/var/lib/mysql @@ -97,14 +101,14 @@ services: environment: - APP_ENV=production - P202_DISABLE_AUTO_UPGRADE=1 - - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL} + - 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} + - 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 From cbdc9d8a1f044d3ecefac844e36fa8014d857a40 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 01:39:01 +0000 Subject: [PATCH 3/3] Derive the image version tag from the git tag, not a semver parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified review finding on tracking202/prosper202#145: version.php's format regex permits values strict SemVer rejects (leading zeros such as 01.2.3, or a 1.2.3-01 prerelease identifier). metadata-action's type=semver rule silently generates no versioned tag for those, so a release could publish only `latest` (stable) or fail the push with an empty tag list (prerelease). The versioned tag now comes straight from the git tag with the v prefix stripped — the release job this job depends on has already validated the tag against version.php, so no re-parse is needed and the versioned tag is always produced. Non-tag workflow_dispatch runs get a type=sha name so the (unpushed) build never has an empty tag list. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SJt817AkfBdUYoXnvaN8vh --- .github/workflows/release.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24c54f64..83d603b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,17 +126,30 @@ jobs: 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=semver,pattern={{version}} + 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.