From 8cd9e3bcf47eb8fe82fb5364c722f5650b8702a6 Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Mon, 20 Apr 2026 14:14:50 +0200 Subject: [PATCH] chore: bootstrap furtka-apps catalog repo Initial layout: apps/fileshare/ (seeded from daniel/furtka apps/), CI (JSON + manifest validator + shellcheck), release pipeline (tag-driven, mirrors core repo), vendored manifest schema for offline validation. The core repo (daniel/furtka) at 26.6-alpha keeps apps/fileshare as a seed so offline first-boot still has an installable app; this catalog becomes authoritative once a box has synced at least once. Co-Authored-By: Claude Opus 4.7 (1M context) --- .forgejo/workflows/ci.yml | 34 +++++++ .forgejo/workflows/release.yml | 28 ++++++ .gitignore | 4 + CHANGELOG.md | 24 +++++ README.md | 70 +++++++++++++++ apps/README.md | 113 +++++++++++++++++++++++ apps/fileshare/.env.example | 2 + apps/fileshare/docker-compose.yaml | 39 ++++++++ apps/fileshare/icon.svg | 9 ++ apps/fileshare/manifest.json | 27 ++++++ scripts/build-catalog-tarball.sh | 47 ++++++++++ scripts/publish-catalog-release.sh | 96 ++++++++++++++++++++ scripts/validate-catalog.py | 124 +++++++++++++++++++++++++ scripts/vendor/README.md | 1 + scripts/vendor/furtka_manifest.py | 140 +++++++++++++++++++++++++++++ 15 files changed, 758 insertions(+) create mode 100644 .forgejo/workflows/ci.yml create mode 100644 .forgejo/workflows/release.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 apps/README.md create mode 100644 apps/fileshare/.env.example create mode 100644 apps/fileshare/docker-compose.yaml create mode 100644 apps/fileshare/icon.svg create mode 100644 apps/fileshare/manifest.json create mode 100755 scripts/build-catalog-tarball.sh create mode 100755 scripts/publish-catalog-release.sh create mode 100755 scripts/validate-catalog.py create mode 100644 scripts/vendor/README.md create mode 100644 scripts/vendor/furtka_manifest.py diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..8552ad3 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate JSON files + run: | + set -e + for f in $(find . -name '*.json' -not -path './node_modules/*'); do + echo "Validating $f" + python3 -m json.tool "$f" > /dev/null + done + + - name: Validate apps/ (manifest schema + compose shape) + run: python3 scripts/validate-catalog.py + + shellcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install shellcheck + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends shellcheck + - name: Run shellcheck + run: shellcheck scripts/*.sh diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..8deebac --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release + +# Tag-triggered: when `git push origin ` lands, this builds the +# catalog tarball and publishes it + the sha256 + release.json to the +# Forgejo releases page for that tag. Running Furtka boxes pull from here +# on their daily furtka-catalog-sync.timer. +# +# Version tags only (CalVer like 26.0, 26.1-alpha, 27.0-beta). Random tags +# are ignored by the [0-9]* prefix. +on: + push: + tags: ['[0-9]*'] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # changelog section extraction needs history + + - name: Build catalog tarball + run: ./scripts/build-catalog-tarball.sh "${GITHUB_REF_NAME}" + + - name: Publish to Forgejo releases + env: + FORGEJO_TOKEN: ${{ secrets.FORGEJO_RELEASE_TOKEN }} + run: ./scripts/publish-catalog-release.sh "${GITHUB_REF_NAME}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb077d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +__pycache__/ +*.pyc +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e1de2b5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to the Furtka apps catalog will be documented here. +Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +Versioning: CalVer (`YY.N`) — same scheme as the core Furtka repo. + +## [Unreleased] + +## [26.6-alpha] - 2026-04-20 + +### Added + +- Initial catalog release. Carries one app: **fileshare** (v0.1.1, SMB + share over `dperson/samba`). Copied from the `daniel/furtka` seed so + boxes on 26.6 see the same fileshare bits whether they pull the + catalog or fall back to the bundled seed. +- Release pipeline + CI parity with the core repo: + `scripts/build-catalog-tarball.sh`, `scripts/publish-catalog-release.sh`, + `scripts/validate-catalog.py` (CI guardrail — loads every manifest via + the vendored `furtka.manifest.load_manifest` + cross-checks compose + volume references). + +[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka-apps/compare/26.6-alpha...HEAD +[26.6-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.6-alpha diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1df508 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# furtka-apps + +Apps catalog for [Furtka](https://furtka.org). Each release here ships a +tarball that running Furtka boxes pull via the daily catalog-sync timer +(see `furtka.catalog` in the core repo). Apps update on their own cadence, +independent of Furtka's core OS release schedule. + +## Repo layout + +``` +apps/ # one folder per app + / + manifest.json # JSON schema — see ./apps/README.md + docker-compose.yaml + .env.example # if the app has user-facing settings + icon.svg +scripts/ + build-catalog-tarball.sh # bundle apps/ + VERSION into furtka-apps-.tar.gz + publish-catalog-release.sh # upload tarball + sha256 + release.json to Forgejo + validate-catalog.py # CI guardrail — manifest + compose syntax + vendor/ + furtka_manifest.py # copy of daniel/furtka furtka/manifest.py +.forgejo/workflows/ + ci.yml # JSON + manifest + shellcheck on every push/PR + release.yml # tag push → build tarball → publish to Forgejo releases +``` + +## Adding an app + +See [apps/README.md](./apps/README.md) for the manifest schema, volume +namespacing rules, `.env.example` guardrails, and the SVG sanitiser +limits the Furtka UI enforces on `icon.svg`. + +Shape of a new app PR: + +1. Add `apps//` with all four files. +2. `python3 scripts/validate-catalog.py` locally — must go green. +3. Open a PR. CI runs the same validator + shellcheck. +4. Merged to `main` — next catalog release will include it. + +## Cutting a release + +Follows the same CalVer + tag-driven pattern as daniel/furtka: + +```bash +# Bump CHANGELOG.md [Unreleased] → [] section, commit: +git commit -m "chore: release " +git tag -a -m "Release " +git push origin main +git push origin +``` + +Tag push fires `.forgejo/workflows/release.yml`. Needs +`FORGEJO_RELEASE_TOKEN` secret (PAT with `write:repository` on this repo). + +## Trust model + +Releases are unsigned (SHA256 sidecar only). Boxes verify the sidecar +against the downloaded tarball before extracting anything. Same posture +as daniel/furtka core releases — PGP is a deferred cross-repo migration. + +## Relationship to daniel/furtka + +- The core repo still ships `apps/fileshare/` as a seed inside the + Furtka release tarball. That's the offline/first-boot fallback. +- When a box has synced the catalog at least once, the resolver + (`furtka.sources.resolve_app_name`) prefers the catalog copy. +- Already-installed apps don't auto-reinstall on catalog updates — user + hits "Reinstall" in `/apps` when they want the new version. Settings + are merge-preserved. diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..13e750f --- /dev/null +++ b/apps/README.md @@ -0,0 +1,113 @@ +# Building a Furtka app from a Docker image + +A Furtka app is a folder with four files. The reconciler walks `/var/lib/furtka/apps/*` at boot, validates each manifest, ensures the declared volumes exist, and runs `docker compose up -d` per app. Filesystem is the only source of truth — no database. + +Use `apps/fileshare/` as the reference implementation. + +## Folder layout + +``` +apps// + manifest.json # required — app metadata and user-facing settings + docker-compose.yaml # required — filename is .yaml, not .yml + .env.example # required — keys consumed by docker-compose, with safe defaults + icon.svg # required — referenced by manifest.icon +``` + +The folder name must equal `manifest.name`. The scanner rejects mismatches. + +## `manifest.json` + +All top-level fields except `description_long` and `settings` are required. + +```json +{ + "name": "myapp", + "display_name": "My App", + "version": "0.1.0", + "description": "One-line summary shown in the app list.", + "description_long": "Longer German prose shown on the app page. Optional.", + "volumes": ["data"], + "ports": [8080], + "icon": "icon.svg", + "settings": [ + { + "name": "ADMIN_PASSWORD", + "label": "Passwort", + "description": "Wird beim ersten Start gesetzt.", + "type": "password", + "required": true + } + ] +} +``` + +Rules enforced by `furtka/manifest.py`: + +- `volumes` — short names, strings. Namespaced to `furtka__` at runtime. +- `ports` — integers. Informational only; compose owns the actual port binding. +- `settings[].name` — must match `^[A-Z_][A-Z0-9_]*$`. This name becomes both the env-var key and the form-field ID. +- `settings[].type` — one of `text`, `password`, `number`. +- `settings[].required` — if true, the install refuses when the value is empty. +- `settings[].default` — optional string. Used to pre-fill the form and the bootstrapped `.env`. + +## `docker-compose.yaml` + +- File extension is `.yaml`. The compose runner hardcodes this — `.yml` will not be found. +- Reference manifest volumes as `furtka__` with `external: true`. The reconciler creates the volume *before* `compose up`, so compose must not try to manage its lifecycle. +- Values from `.env` are substituted by compose in the usual `${VAR}` form. +- If the upstream image ships a HEALTHCHECK that misbehaves on Furtka's setup, disable it — a permanently-unhealthy container scares users reading `docker ps`. +- Pin images to a digest or stable tag when you can. `:latest` is acceptable for an MVP but noisy. + +Minimal example: + +```yaml +services: + app: + image: ghcr.io/example/myapp:1.2.3 + restart: unless-stopped + environment: + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + ports: + - "8080:8080" + volumes: + - furtka_myapp_data:/var/lib/myapp + +volumes: + furtka_myapp_data: + external: true +``` + +## `.env.example` + +One `KEY=VALUE` per line. Every key declared in `manifest.settings` should have a line here so the compose file resolves cleanly on first install even before the user opens the form. + +Do not use `changeme` (or any value listed in `furtka.installer.PLACEHOLDER_SECRETS`) as the default for a required secret. The install step scans the final `.env` and refuses to finish if a placeholder survives — this is the guardrail that stops us shipping an app with a known password. + +For non-secret values (usernames, paths), sensible defaults are fine and go straight into `.env` on first install. + +## `icon.svg` + +- 64×64 viewBox, no width/height attributes so the UI can scale it. +- Use `fill="currentColor"` (and `stroke="currentColor"`) so the icon picks up the current theme instead of baking in a color. +- Keep it single-path-ish. These render small in the app grid. +- The icon is inlined into the `/apps` page by the defensive SVG sanitiser, which strips `