{"id":35568310,"url":"https://github.com/tkn777/retentions","last_synced_at":"2026-02-22T16:32:51.038Z","repository":{"id":323049846,"uuid":"1091941622","full_name":"tkn777/retentions","owner":"tkn777","description":"Cross-platform CLI tool for file retention using backup-style time-based rules","archived":false,"fork":false,"pushed_at":"2026-01-31T15:40:53.000Z","size":999,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-02-01T02:00:43.213Z","etag":null,"topics":["auditing","automation","backups","cleanup","cli","devops","file-management","iso27001","retention"],"latest_commit_sha":null,"homepage":"","language":"Python","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/tkn777.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","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},"funding":{"custom":["https://paypal.me/kuhlmannthomas"]}},"created_at":"2025-11-07T18:45:37.000Z","updated_at":"2026-01-31T15:40:57.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tkn777/retentions","commit_stats":null,"previous_names":["tkn777/retentions"],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/tkn777/retentions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkn777%2Fretentions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkn777%2Fretentions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkn777%2Fretentions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkn777%2Fretentions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tkn777","download_url":"https://codeload.github.com/tkn777/retentions/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkn777%2Fretentions/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28990733,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-01T20:57:35.821Z","status":"ssl_error","status_checked_at":"2026-02-01T20:57:29.580Z","response_time":56,"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":["auditing","automation","backups","cleanup","cli","devops","file-management","iso27001","retention"],"created_at":"2026-01-04T16:14:02.563Z","updated_at":"2026-02-22T16:32:51.020Z","avatar_url":"https://github.com/tkn777.png","language":"Python","funding_links":["https://paypal.me/kuhlmannthomas"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"left\"\u003e\n  \u003cimg src=\"resources/retentions-logo.png\" alt=\"retentions – deterministic file retention CLI tool\" height=110\u003e\n\u003c/p\u003e\n\n## retentions\n\nA small, feature-rich cross-platform CLI tool to apply backup-style retention rules to any file set. \n\nIt remains only the most recent or representative files according to simple time-based rules and deletes the rest.\n\n```bash\nretentions /data/backups '*.tar.gz' -d 7 -w 4 -m 6   # Keeps last 7 days, 4 weeks and 6 months\n```\n\nIt is a safe alternative to ad-hoc cleanup scripts for backups, archives, and artifacts.\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![GitHub release](https://img.shields.io/github/v/release/tkn777/retentions)](https://github.com/tkn777/retentions/releases)\n[![Platform](https://img.shields.io/badge/platform-linux%20|%20macos%20|%20windows-lightgrey)]()\n[![coverage](https://codecov.io/gh/tkn777/retentions/branch/master/graph/badge.svg)](https://codecov.io/gh/tkn777/retentions)\n\n---\n\n## 📌 Why `retentions` exists\n\nRetention is rarely the problem — implicit deletion usually is.\n\n- How do we make retention decisions auditable and reproducible?\n- Why do time-based cleanup scripts so often delete the *wrong* backups?\n- How can deletion decisions be explained, reviewed, and justified later?\n\n`retentions` was built to answer these questions with explicit rules instead of implicit assumptions — treating retention as policy expressed in code.\n\n---\n\n## 🌐 Overview  \n\n`retentions` is a single-file Python script that applies retention logic to a directory of files. \n\nIt groups files into time **buckets** (hours, days, weeks, months, quarters, years) and retains only one representative file per bucket, typically the most recent one. Additionally it retains the **last N files** regardless of age.\n\nAnd filters can be used to reduce the retention scope to a subset of files.\n\nEverything outside your defined retention scope is deleted (unless `--dry-run` or `--list-only` is used).\n\n⚠️ **Warning:** This tool permanently deletes files outside the defined retention scope.  \nUse `--dry-run` first to verify behavior. The author assumes no liability for data loss.\n\nIt is intended for administrators who understand retention concepts and want deterministic, scriptable behavior.\n\n---\n\n## ❕ Folder mode semantics\n\nWhen `--folder-mode` is enabled (see [Folder Mode](#-folder-mode---folder-mode) ), **all references to 'files' in this whole documentation in this project refer to 'folders' instead**.\n\n---\n\n## ⚙️ Features\n\n- Pure **Python 3**, no external dependencies.  \n- Runs on **Linux, macOS, and Windows** (and anywhere else Python 3 runs).  \n- Supports **hourly, daily, weekly, monthly, quarterly, yearly** retention buckets.  \n- Supports **keeping the last N files** (`--last`) regardless of age.  \n- Supports **filtering (max-age, max-size, max-files)** to reduce the retention scope to a subset of files.\n- Supports **protecting files** against deletion.\n- Supports **regex or glob** pattern matching.  \n- Safe modes:  \n  - `--dry-run` → simulate actions  \n  - `--list-only` → output only files that would be deleted  \n- Usage:\n  - `--help` (also described below in this document)\n- Version:\n  - `--version`\n- Clean, deterministic output - ASCII only, no colors, no locales.\n\n---\n\n## 🧠 Logic Summary\n\n### Logic\n1. Scan all files\n2. Ignore all protected files (for the whole process)\n3. Retain (or not) by time-based buckets (--hours, --days, --weeks, --months, --years, --quarter, --week13)\n4. Retain by --last (latest N files)\n5. Filtered (everything retained before) by \n    1. --max-age (strict time cutoff)\n    2. --max-files (upper limit by file count)\n    3. --max-size (upper limit by total storage)\n6. Delete files or list files to delete\n7. If `--dry-run` is enabled, print the planned actions instead of executing them.\n\nIn this document, \"retain\" refers to the selection process, while \"keep\" refers to the resulting decision on individual files.\n\n---\n\n## 🧱 Non-goals\n\n`retentions` is intentionally **not** designed to:\n- perform recursive directory traversal\n- act as a full backup solution\n- manage retention across multiple directories\n- provide interactive safety prompts\n\n---\n\n## 🧰 Installation\n\nYou can install **retentions** in several ways, depending on your system and preference.\n\n*All installation methods require **Python 3.9+** *(tested against Python 3.9-3.14)*.\\\nNo dependencies beyond Python 3.*\n\n\n### 🧩 Option 1 – Debian Package (.deb)\n\nDownload the latest `.deb` package from the [Releases](https://github.com/tkn777/retentions/releases) page and install it manually:\n\n```bash\nsudo dpkg -i retentions_x.y.z_all.deb\n```\n\nThis installs:\n- `/usr/bin/retentions`\n- documentation in `/usr/share/doc/retentions/`\n- `man` page\n- shell completion for `bash` and `zsh`\n\n#### 🗄️ Debian Repository\n\nYou can use the Debian repository provided by `thk-systems.net` to receive automatic updates:\n\n```bash\ncurl -fsSL https://debian.thk-systems.net/repo-install.sh | sudo sh\nsudo apt install retentions\n```\n\n### 📦 Option 2 – Redhat Package (.rpm)\n\nDownload the latest `.rpm` package from the [Releases](https://github.com/tkn777/retentions/releases) page and install it manually:\n\n```bash\nsudo dnf install retentions-x.y.z-n.noarch.rpm # or with yum\n```\n\nThis installs:\n- `/usr/bin/retentions`\n- documentation in `/usr/share/doc/retentions/`\n- `man` page\n- shell completion for `bash` and `zsh`\n\n### 📦 Option 3 – Self-installing linux installer\n\nDownload the latest `.linux-installer` package from the [Releases](https://github.com/tkn777/retentions/releases) page and install it manually:\n\n```bash\nsudo bash retentions-x.y.z.linux-installer\n```\n\nThis installs:\n- `/usr/local/bin/retentions`\n- documentation, `man` page and shell completion in `/usr/local/share/...`\n\n### 🗜️ Option 4 – Universal (tar.gz / zip)\n\nFor non-Debian-based or non-Redhat-based systems:\n\nDownload the latest `retentions-x.y.z.tar.gz` or `retentions-x.y.z.zip` package from the [Releases](https://github.com/tkn777/retentions/releases) page and install it manually\n\nThe archive includes:\n- the common Python script: `retentions.py`\n- a common linux variant with shebang: `linux/retentions`\n- a macOS variant with shebang: `macos/retentions`\n- and all the docs: `docs`\n- and the shell completions for `bash` and `zsh`\n\n### 🔍 To verify installation:\n\n```bash\nretentions --help\n```\n\n---\n\n## 🖥️ Usage\n\n```bash\npython3 retentions.py \u003cpath\u003e \u003cfile_pattern\u003e [options]\n```\n\n*If you installed via .deb or .rpm or a shebang'ed version from the tar.gz / zip, you can simply run `retentions` instead of `python3 retentions.py`.*\n\n---\n\n## 🔧 Arguments and Options\n\n| Arguments | Description |\n|--------|--------------|\n| `path` | base directory to scan |\n| `file_pattern` | glob pattern for matching files (use quotes to prevent shell expansion) |\n\n⚠️ `path` and `file_pattern` are mandatory\n\u0026nbsp;\n\n| Flag | Description |\n|--------|--------------|\n| `-r, --regex [mode]` | file_pattern / protect is a regex (otherwise: glob pattern) - mode: casesensitive (default), ignorecase |\n| `--age-type [time]` | Used time attribute for file age - time: ctime, mtime (default), atime, birthtime |\n| `--protect \u003cpattern\u003e` | Protect files from deletion (using regex or glob, like file_pattern) |\n| `--folder-mode` | Use folders instead of files in `path`: You need to specify the mode, to get the xtime of the folder: folder, youngest-file (default), oldest-file, path=\u003cpath\u003e), youngest-|oldest-file are recursive within the folder |\n\n⚠️ `age-type`: \n- `ctime` is platform-dependent: e.g. Windows =\u003e create-time, Linux =\u003e change time of meta data\n- `atime` may not reliable, check your platform, your filesystem and your mount options\n- `birthtime` may not available on your platform or your filesystem\n- ❗`retentions` checks the availability of the selected `age-type`, but not the semantic\n\n💡 See section [Folder Mode](#-folder-mode---folder-mode) for details about `--folder-mode`\n\u0026nbsp;\n\n| Retention options | Description |\n|--------|--------------|\n| `-h, --hours \u003cint\u003e` | Retain one file per hour from the last N hours |\n| `-d, --days \u003cint\u003e` | Retain one file per day from the last N days |\n| `-w, --weeks \u003cint\u003e` | Retain one file per week from the last N weeks |\n| `-m, --months \u003cint\u003e` | Retain one file per month from the last N months |\n| `-q, --quarters \u003cint\u003e` | Retain one file per quarter from the last N quarters (quarter by months) |\n| `--week13 \u003cint\u003e` | Retain one file per 13-week block from the last N 13-week blocks (quarter by weeks) |\n| `-y, --years \u003cint\u003e` | Retain one file per year from the last N years |\n| `-l, --last \u003cint\u003e` | Always retains the N most recently modified files |\n| `-i, --skip-by-filesize \u003csize\u003e` | Skip (and delete) files smaller than \u003csize\u003e (e.g. 12, 2K, 1.5M) |\n\n📝 Every retention option can be combined with any (or all) others\n\u0026nbsp;\n\n🧠 Logic:\n- The retention periods are applied cumulatively. For example, a file that is marked as keep with the retention `--days` **cannot** be marked as keep with the retention `--week`.\n- Retention buckets are hierarchical. Coarser buckets only select data strictly older than all finer-grained selections\n- One exception here is `--last`. It always marks the last `N` files as retained, regardless of all other retentions.\n- If no retention period are specified all files are retained (and may be filtered)\n\u0026nbsp;\n\n| Filter options | Description |\n|--------|--------------|\n| `-s, --max-size \u003csize\u003e` | Keep maximum within total size (e.g. 12, 10.5M, 500G, 3E) |\n| `-f, --max-files \u003ccount\u003e` | Keep maximum total files |\n| `-a, --max-age \u003cage\u003e` | Keep maximum within time span N from script start (e.g. 3600, 1h, 1d, 1w, 1m, 1q, 1y) |\n\n📝 Every filter option can be combined with any (or all) others\n\u0026nbsp;\n\n🧠 Logic:\n- Filters apply after retention and may delete files previously marked to keep. (Use `--verbose debug` and `--dry-run` to see decision history.)\n\u0026nbsp;\n\n| Behavior options | Description |\n|--------|--------------|\n| `-L, --list-only [separator]` | Output only file paths that would be deleted (incompatible with --verbose, separator defaults to '\\n') |\n| `-V, v, --verbose \u003cint\u003e` | Verbosity level: 0 = error, 1 = warn, 2 = info, 3 = debug (default: 'info', if specified without value; 'error' otherwise; use numbers or names) |\n| `-X, --dry-run` | Show planned actions but do not delete any files |\n| `--no-lock-file` | Omit lock file (default: enabled) |\n| `--fail-on-delete-error` | Fails and exits if a file could not be deleted (default: disabled and print warning) |\n\n💡 Using `--dry-run` is a good option to start with `retentions` 😏\n\u0026nbsp; \n\n| Expert options | Description |\n|--------|--------------|\n| `--delete-companions [rules]` | Delete companion files defined by the rules (prefix\\|suffix:match:companions, e.g. 'suffix:tar.gz:sha256,md5') |\n\n💡 See section [Delete Companions](#-delete-companions---delete-companions) for details\n\u0026nbsp;\n\n| Developer options | Description |\n|--------|--------------|\n| `--stacktrace` | Add output of stacktrace in case of errors |\n\n📝 For developers only\n\u0026nbsp;\n\n| Common arguments | Description |\n|--------|--------------|\n| `-H, --help` | Show the help / usage of `retentions` |\n| `-R, --version` | Show the version of `retentions` |\n\n💡 Common arguments can be used without any other arguments.\n\n---\n\n### 🧾 Examples \u0026 further notes\n\n#### Retentions \u0026 Filters\n\n```bash\n# Retain last 7 days, 4 weeks, 6 months\npython3 retentions.py /data/backups '*.tar.gz' -d 7 -w 4 -m 6\n```\n\n```bash\n# Keep daily/weekly/monthly backups, but retain at most 10 files in total.\n# Older retained files are deleted if the limit is exceeded.\npython3 retentions.py /data/backups '*.tar.gz' -d 7 -w 4 -m 6 --max-files 10 --dry-run\n```\n\n```bash\n# Keep up to 12 monthly backups, but limit total retained size to 50 GB.\n# Older retained files are deleted once the size limit is exceeded.\npython3 retentions.py /data/backups '*.tar.gz' -m 12 --max-size 50G --dry-run\n```\n\n#### Dry run (no deletion)\n\n```bash\npython3 retentions.py /data/backups '*.tar.gz' -d 10 -w 3 -m 6 --dry-run\n```\n\n#### List-only mode (for piping into other tools)\n\n```bash\npython3 retentions.py /data/backups '*.tar.gz' -d 5 -w 12 --list-only '\\0' | xargs -0 rm\n```\n⚠️ This bypasses retentions' own deletion **safety mechanisms**. Use with care!\n\n#### Verbose output\n\n```bash\n# Detailed logging\npython3 retentions.py /data/backups '*.tar.gz' -d 3 -w 1 --verbose debug\n```\n\n#### 🗓️ Hierarchical calendar-based retention example\n\nRetention levels are applied hierarchically by calendar containment, not additively by age.\n\nEach coarser-grained retention level only selects data strictly *outside* the\ncalendar range already covered by finer-grained levels.\n\nExample:\n\n```\n-d 1 -w 1 -m 1\n```\n\nAssume the most recent retained day is: `2026-01-31`\n\nThis day lies in:\n- calendar week **2026-W05**\n- calendar month **2026-01**\n\nRetention selection therefore proceeds as follows:\n\n- **Days (`-d 1`)**\n  - keep: `2026-01-31`\n\n- **Weeks (`-w 1`)**\n  - select a week *before* calendar week `2026-W05`\n  - the week containing the retained day is excluded\n  - the first eligible week is **2026-W04**\n\n- **Months (`-m 1`)**\n  - select a month *before* calendar month `2026-01`\n  - the month containing the retained week is excluded\n  - the first eligible month is **2025-12**\n\nIf no data exists in these older calendar buckets, no additional items are kept.\n\nThis hierarchical approach ensures that:\n- finer-grained retention always takes precedence,\n- coarser-grained retention never overlaps with finer levels,\n- and each retention level contributes unique temporal coverage.\n\u0026nbsp;\n\n```text\nTime ──────────────────────────────────────────────────────────────▶\n\nMonth:   2025-12        |        2026-01\n         [ eligible ]   | [ excluded by weeks ]\n                         ┌──────────────────────-─────────┐\nWeek:                    │   2026-W05  (excluded)         │\n                         │   2026-W04  ← selected (weeks) │\n                         └─────────────────────-──────────┘\nDay:                                   2026-01-31 ← selected (days)\n```\n\n---\n\n### 📁 Folder mode (`--folder-mode`)\n\nBy default, `retentions` applies retention rules only to files.\nWith `--folder-mode`, retention rules are applied to **directories instead**.\n\nOnly **top-level directories** in the base directory are considered.\nSubdirectories are never selected as retention objects.\n\nThe age of a directory is determined by one of the following time references:\n\n- `youngest-file` (default):  \n  Uses the most recent file inside the directory (recursive).\n\n- `oldest-file`:  \n  Uses the oldest file inside the directory (recursive).\n\n- `folder`:  \n  Uses the directory’s own timestamp (`mtime`, `ctime`, etc.).\n\n- `path=\u003cpath\u003e`:\n  Uses the given file's timestamp\n\nEmpty directories are ignored (with a warning).\nRecursive traversal is used only to determine a directory’s age.\n\n---\n\n### 🔗 Delete Companions (`--delete-companions`)\n\nDeletes *companion files* together with a file being removed (e.g. checksum or metadata files).\n\n#### Rule-Definition\n\n```\nTYPE:MATCH:COMPANIONS\n```\n\n- `TYPE`: `suffix` or `prefix`\n- `MATCH`: prefix or suffix to match (may be empty → always matches)\n- `COMPANIONS`: comma-separated replacements\n\n#### Rule-Examples:\n\n```\n--delete-companions \"suffix:.tar:.md5,.info\" \n# Deletes `archive.md5` and `archive.info` when deleting `archive.tar`.\n\n--delete-companions \"suffix::.bak\" \"suffix::.tmp\" \n# Deletes `file.txt.bak` and `file.txt.tmp` when deleting `file.txt`.\n\n--delete-companions \"prefix:backup-:meta-\" \n# Deletes `meta-data.tar` when deleting `backup-data.tar`\n```\n\n#### Notes\n\n- Only existing regular files are deleted.\n- Companion paths are deduplicated.\n- Protected or otherwise disallowed companions abort deletion (e.g. files marked as kept or pruned).\n- Rules must be quoted.\n\n---\n\n### ➡️ Symbolic links\n\nSymbolic links are intentionally **not supported**.\n\nAll paths must refer to real filesystem objects.  \n\nThis restriction ensures a strict, predictable retention scope and avoids ambiguous or hard-to-reason-about filesystem behavior.\n\n---\n\n### ⚠️ Quoting File Patterns\n\nAlways **quote your file patterns** (and companion rules) when calling `retentions`.\n\nIf you omit the quotes, your shell (e.g. Bash, Zsh, PowerShell) will expand the pattern **before** it reaches the program, resulting in unexpected arguments or errors.\n\n#### ✅ Correct\n```bash\npython3 retentions.py /data/backups '*.tar.gz'\npython3 retentions.py /data/logs 'log-*.txt'\npython3 retentions.py /data/temp '.*\\\\.bak'\n```\n\n#### ❌ Incorrect\n```bash\npython3 retentions.py /data/backups *.tar.gz\n```\n(your shell expands *.tar.gz before retentions runs)\n\nretentions itself handles pattern matching internally using glob or regex, so quoting ensures the pattern is passed as intended.\n\n--- \n\n### ⚡ Exit Codes\n\n| Code | Meaning |\n|------|----------|\n| 0 | Execution successful |\n| 1 | I/O or filesystem error |\n| 2 | Invalid or conflicting arguments |\n| 5 | Concurrency error |\n| 7 | Integrity check failed |\n| 9 | Unexpected error |\n\n\n---\n\n### 💡 Tip\n\nUse `--list-only` to integrate with external scripts or pipelines:\n\n```bash\npython3 retentions.py /data/logs '*.log' -d 3 -w 2 --list-only | while read f; do\n    echo \"Would delete $f\"\ndone\n```\n\n---\n\n## 🧭 Known Issues \u0026 Trade-offs\n\nThe following behaviors are known, intentional trade-offs rather than bugs.\n\n- Retention option `months`, `quarters` and `years` are truly months, quarters and years, but filter options are \n  - `xm` =\u003e x * 30 days\n  - `xq` =\u003e x * 90 days\n  - `xy` =\u003e x * 365 days\n\n---\n\n### ~~☢️~~ Non-atomic behavior\n\nretentions does not operate atomically. Since the underlying filesystem is not atomic, directory contents may change while retentions is scanning or evaluating files.\nThis can lead to inconsistent or incorrect results if the target path is modified concurrently.\nIf you require a strictly consistent state, ensure that the directory is not modified during execution.\n\nBy default retentions writes a lock file `.retentions.lock` - This can be used by other tools.\n\n---\n\n## 🛠️ Troubleshooting\n\n- No files are deleted: Check retention options and filters, and use `--verbose debug --dry-run`.\n- Unexpected deletions: Review the decision log output (`--verbose debug`).\n- Script aborts early: Check exit codes and error messages.\n\n---\n\n## 🪨 Project Status\n\nretentions is a deliberately small, complete tool.\n\nIts feature set is stable and sufficient for its intended purpose.\nFuture changes will primarily consist of bug fixes, portability improvements, or documentation updates.\n\nA low commit frequency is therefore a sign of maturity, not abandonment.\n\n---\n\n## 🪶 License\n\nMIT License  \nCopyright © 2025-2026 Thomas Kuhlmann\n\n---\n\n## 🐈‍⬛ Dedication\n\nThis project is dedicated to 'Motzli' - my real-life companion and friend. To that quiet, resilient cat whose presence still lives within me and keeps me standing up, no matter how often life burns everything down to ashes. For the strength, the gentleness, and the stubborn hope that never leaves - and will never be forgotten.\n\n---\n\nSimple. Predictable. Cross-platform.  \nJust **retentions**.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftkn777%2Fretentions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftkn777%2Fretentions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftkn777%2Fretentions/lists"}