diff options
| -rw-r--r-- | about/resources.html | 202 | ||||
| -rw-r--r-- | about/showcase.html | 427 | ||||
| -rw-r--r-- | about/showcase/debroid/image-1.png | 60 | ||||
| -rw-r--r-- | gemfeed/DRAFT-f3s-kubernetes-with-freebsd-part-X.html | 1212 |
4 files changed, 873 insertions, 1028 deletions
diff --git a/about/resources.html b/about/resources.html index 204b2040..3946e2cd 100644 --- a/about/resources.html +++ b/about/resources.html @@ -51,112 +51,112 @@ <span>In random order:</span><br /> <br /> <ul> -<li>Raku Fundamentals; Moritz Lenz; Apress</li> -<li>Go Brain Teasers - Exercise Your Mind; Miki Tebeka; The Pragmatic Programmers</li> -<li>Effective Java; Joshua Bloch; Addison-Wesley Professional</li> -<li>C++ Programming Language; Bjarne Stroustrup;</li> -<li>Hands-on Infrastructure Monitoring with Prometheus; Joel Bastos, Pedro Araujo; Packt </li> -<li>Funktionale Programmierung; Peter Pepper; Springer</li> -<li>Systemprogrammierung in Go; Frank Müller; dpunkt</li> -<li>Polished Ruby Programming; Jeremy Evans; Packt Publishing</li> -<li>100 Go Mistakes and How to Avoid Them; Teiva Harsanyi; Manning Publications</li> +<li>Object-Oriented Programming with ANSI-C; Axel-Tobias Schreiner</li> +<li>Terraform Cookbook; Mikael Krief; Packt Publishing</li> +<li>Pro Puppet; James Turnbull, Jeffrey McCune; Apress</li> +<li>Programming Perl aka "The Camel Book"; Tom Christiansen, brian d foy, Larry Wall & Jon Orwant; O'Reilly</li> +<li>Amazon Web Services in Action; Michael Wittig and Andreas Wittig; Manning Publications</li> <li>Concurrency in Go; Katherine Cox-Buday; O'Reilly</li> +<li>Seeking SRE: Conversations About Running Production Systems at Scale; David N. Blank-Edelman; eBook</li> +<li>C++ Programming Language; Bjarne Stroustrup;</li> +<li>Distributed Systems: Principles and Paradigms; Andrew S. Tanenbaum; Pearson</li> +<li>Raku Fundamentals; Moritz Lenz; Apress</li> +<li>The Kubernetes Book; Nigel Poulton; Unabridged Audiobook</li> +<li>Tmux 2: Productive Mouse-free Development; Brain P. Hogan; The Pragmatic Programmers </li> +<li>Higher Order Perl; Mark Dominus; Morgan Kaufmann</li> +<li>Learn You a Haskell for Great Good!; Miran Lipovaca; No Starch Press</li> +<li>Learn You Some Erlang for Great Good; Fred Herbert; No Starch Press</li> <li>Modern Perl; Chromatic ; Onyx Neon Press</li> -<li>The Practise of System and Network Administration; Thomas A. Limoncelli, Christina J. Hogan, Strata R. Chalup; Addison-Wesley Professional Pro Git; Scott Chacon, Ben Straub; Apress</li> +<li>DevOps And Site Reliability Engineering Handbook; Stephen Fleming; Audible</li> +<li>Chaos Engineering - System Resiliency in Practice; Casey Rosenthal and Nora Jones; eBook</li> +<li>Funktionale Programmierung; Peter Pepper; Springer</li> <li>21st Century C: C Tips from the New School; Ben Klemens; O'Reilly</li> -<li>Terraform Cookbook; Mikael Krief; Packt Publishing</li> +<li>Java ist auch eine Insel; Christian Ullenboom; </li> +<li>Think Raku (aka Think Perl 6); Laurent Rosenfeld, Allen B. Downey; O'Reilly</li> <li>The DevOps Handbook; Gene Kim, Jez Humble, Patrick Debois, John Willis; Audible</li> -<li>Seeking SRE: Conversations About Running Production Systems at Scale; David N. Blank-Edelman; eBook</li> -<li>Chaos Engineering - System Resiliency in Practice; Casey Rosenthal and Nora Jones; eBook</li> +<li>Hands-on Infrastructure Monitoring with Prometheus; Joel Bastos, Pedro Araujo; Packt </li> +<li>Perl New Features; Joshua McAdams, brian d foy; Perl School</li> +<li>Site Reliability Engineering; How Google runs production systems; O'Reilly</li> +<li>Go Brain Teasers - Exercise Your Mind; Miki Tebeka; The Pragmatic Programmers</li> <li>Data Science at the Command Line; Jeroen Janssens; O'Reilly</li> -<li>DNS and BIND; Cricket Liu; O'Reilly</li> -<li>Kubernetes Cookbook; Sameer Naik, Sébastien Goasguen, Jonathan Michaux; O'Reilly</li> -<li>The Docker Book; James Turnbull; Kindle</li> -<li>Think Raku (aka Think Perl 6); Laurent Rosenfeld, Allen B. Downey; O'Reilly</li> +<li>Programming Ruby 3.3 (5th Edition); Noel Rappin, with Dave Thomas; The Pragmatic Bookshelf</li> <li>Effective awk programming; Arnold Robbins; O'Reilly</li> <li>Raku Recipes; J.J. Merelo; Apress</li> -<li>DevOps And Site Reliability Engineering Handbook; Stephen Fleming; Audible</li> -<li>The Go Programming Language; Alan A. A. Donovan; Addison-Wesley Professional</li> +<li>100 Go Mistakes and How to Avoid Them; Teiva Harsanyi; Manning Publications</li> +<li>DNS and BIND; Cricket Liu; O'Reilly</li> +<li>The Docker Book; James Turnbull; Kindle</li> <li>Leanring eBPF; Liz Rice; O'Reilly</li> -<li>Amazon Web Services in Action; Michael Wittig and Andreas Wittig; Manning Publications</li> +<li>Ultimate Go Notebook; Bill Kennedy</li> +<li>Systems Performance Tuning; Gian-Paolo D. Musumeci and others...; O'Reilly</li> +<li>The Go Programming Language; Alan A. A. Donovan; Addison-Wesley Professional</li> <li>The KCNA (Kubernetes and Cloud Native Associate) Book; Nigel Poulton</li> -<li>Higher Order Perl; Mark Dominus; Morgan Kaufmann</li> -<li>Programming Ruby 3.3 (5th Edition); Noel Rappin, with Dave Thomas; The Pragmatic Bookshelf</li> -<li>Distributed Systems: Principles and Paradigms; Andrew S. Tanenbaum; Pearson</li> +<li>Developing Games in Java; David Brackeen and others...; New Riders</li> +<li>Effective Java; Joshua Bloch; Addison-Wesley Professional</li> <li>Clusterbau mit Linux-HA; Michael Schwartzkopff; O'Reilly</li> +<li>The Practise of System and Network Administration; Thomas A. Limoncelli, Christina J. Hogan, Strata R. Chalup; Addison-Wesley Professional Pro Git; Scott Chacon, Ben Straub; Apress</li> +<li>Kubernetes Cookbook; Sameer Naik, Sébastien Goasguen, Jonathan Michaux; O'Reilly</li> <li>The Pragmatic Programmer; David Thomas; Addison-Wesley</li> -<li>Learn You Some Erlang for Great Good; Fred Herbert; No Starch Press</li> -<li>Systems Performance Tuning; Gian-Paolo D. Musumeci and others...; O'Reilly</li> -<li>Tmux 2: Productive Mouse-free Development; Brain P. Hogan; The Pragmatic Programmers </li> -<li>Perl New Features; Joshua McAdams, brian d foy; Perl School</li> -<li>Site Reliability Engineering; How Google runs production systems; O'Reilly</li> -<li>Java ist auch eine Insel; Christian Ullenboom; </li> -<li>Ultimate Go Notebook; Bill Kennedy</li> <li>97 things every SRE should know; Emil Stolarsky, Jaime Woo; O'Reilly</li> -<li>Pro Puppet; James Turnbull, Jeffrey McCune; Apress</li> -<li>Object-Oriented Programming with ANSI-C; Axel-Tobias Schreiner</li> -<li>Programming Perl aka "The Camel Book"; Tom Christiansen, brian d foy, Larry Wall & Jon Orwant; O'Reilly</li> -<li>The Kubernetes Book; Nigel Poulton; Unabridged Audiobook</li> -<li>Developing Games in Java; David Brackeen and others...; New Riders</li> -<li>Learn You a Haskell for Great Good!; Miran Lipovaca; No Starch Press</li> +<li>Polished Ruby Programming; Jeremy Evans; Packt Publishing</li> +<li>Systemprogrammierung in Go; Frank Müller; dpunkt</li> </ul><br /> <h2 style='display: inline' id='technical-references'>Technical references</h2><br /> <br /> <span>I didn't read them from the beginning to the end, but I am using them to look up things. The books are in random order:</span><br /> <br /> <ul> -<li>The Linux Programming Interface; Michael Kerrisk; No Starch Press </li> +<li>Groovy Kurz & Gut; Joerg Staudemeier; O'Reilly</li> <li>Implementing Service Level Objectives; Alex Hidalgo; O'Reilly</li> <li>Algorithms; Robert Sedgewick, Kevin Wayne; Addison Wesley</li> <li>Relayd and Httpd Mastery; Michael W Lucas</li> +<li>Go: Design Patterns for Real-World Projects; Mat Ryer; Packt</li> <li>BPF Performance Tools - Linux System and Application Observability, Brendan Gregg; Addison Wesley</li> +<li>The Linux Programming Interface; Michael Kerrisk; No Starch Press </li> <li>Understanding the Linux Kernel; Daniel P. Bovet, Marco Cesati; O'Reilly</li> -<li>Go: Design Patterns for Real-World Projects; Mat Ryer; Packt</li> -<li>Groovy Kurz & Gut; Joerg Staudemeier; O'Reilly</li> </ul><br /> <h2 style='display: inline' id='self-development-and-soft-skills-books'>Self-development and soft-skills books</h2><br /> <br /> <span>In random order:</span><br /> <br /> <ul> -<li>Ultralearning; Scott Young; Thorsons</li> -<li>Who Moved My Cheese?; Dr. Spencer Johnson; Vermilion</li> -<li>Eat That Frog; Brian Tracy</li> -<li>The Daily Stoic; Ryan Holiday, Stephen Hanselman; Profile Books</li> -<li>The Complete Software Developer's Career Guide; John Sonmez; Unabridged Audiobook</li> -<li>The Power of Now; Eckhard Tolle; Yellow Kite</li> -<li>Buddah and Einstein walk into a Bar; Guy Joseph Ale, Claire Bloom; Blackstone Publishing</li> +<li>The Bullet Journal Method; Ryder Carroll; Fourth Estate</li> +<li>Coders at Work - Reflections on the craft of programming, Peter Seibel and Mitchell Dorian et al., Audiobook</li> <li>So Good They Can't Ignore You; Cal Newport; Business Plus</li> -<li>Stop starting, start finishing; Arne Roock; Lean-Kanban University </li> -<li>The Obstacle Is The Way; Ryan Holiday; Profile Books Ltd</li> -<li>Time Management for System Administrators; Thomas A. Limoncelli; O'Reilly</li> <li>The Joy of Missing Out; Christina Crook; New Society Publishers</li> -<li>101 Essays that change the way you think; Brianna Wiest; Audiobook</li> -<li>The Bullet Journal Method; Ryder Carroll; Fourth Estate</li> -<li>Solve for Happy; Mo Gawdat (RE-READ 1ST TIME)</li> -<li>Atomic Habits; James Clear; Random House Business</li> <li>Deep Work; Cal Newport; Piatkus</li> -<li>Coders at Work - Reflections on the craft of programming, Peter Seibel and Mitchell Dorian et al., Audiobook</li> -<li>Consciousness: A Very Short Introduction; Susan Blackmore; Oxford Uiversity Press</li> +<li>Search Inside Yourself - The Unexpected path to Achieving Success, Happiness (and World Peace); Chade-Meng Tan, Daniel Goleman, Jon Kabat-Zinn; HarperOne</li> +<li>Staff Engineer: Leadership beyond the management track; Will Larson; Audiobook</li> +<li>The 7 Habits Of Highly Effective People; Stephen R. Covey; Simon & Schuster UK</li> +<li>Buddah and Einstein walk into a Bar; Guy Joseph Ale, Claire Bloom; Blackstone Publishing</li> <li>Getting Things Done; David Allen</li> -<li>The Phoenix Project - A Novel About IT, DevOps, and Helping your Business Win; Gene Kim and Kevin Behr; Trade Select</li> -<li>Meditation for Mortals, Oliver Burkeman, Audiobook</li> -<li>Eat That Frog!; Brian Tracy; Hodder Paperbacks</li> +<li>The Obstacle Is The Way; Ryan Holiday; Profile Books Ltd</li> +<li>Solve for Happy; Mo Gawdat (RE-READ 1ST TIME)</li> +<li>The Software Engineer's Guidebook: Navigating senior, tech lead, and staff engineer positions at tech companies and startups; Gergely Orosz; Audiobook </li> <li>Never Split the Difference; Chris Voss, Tahl Raz; Random House Business</li> +<li>Psycho-Cybernetics; Maxwell Maltz; Perigee Books</li> +<li>Meditation for Mortals, Oliver Burkeman, Audiobook</li> <li>Influence without Authority; A. Cohen, D. Bradford; Wiley</li> +<li>Atomic Habits; James Clear; Random House Business</li> +<li>Soft Skills; John Sommez; Manning Publications</li> +<li>Consciousness: A Very Short Introduction; Susan Blackmore; Oxford Uiversity Press</li> +<li>The Good Enough Job; Simone Stolzoff; Ebury Edge</li> +<li>Stop starting, start finishing; Arne Roock; Lean-Kanban University </li> <li>Ultralearning; Anna Laurent; Self-published via Amazon</li> +<li>Eat That Frog; Brian Tracy</li> +<li>The Daily Stoic; Ryan Holiday, Stephen Hanselman; Profile Books</li> +<li>Ultralearning; Scott Young; Thorsons</li> +<li>Time Management for System Administrators; Thomas A. Limoncelli; O'Reilly</li> <li>Slow Productivity; Cal Newport; Penguin Random House</li> -<li>Psycho-Cybernetics; Maxwell Maltz; Perigee Books</li> -<li>Digital Minimalism; Cal Newport; Portofolio Penguin</li> -<li>Soft Skills; John Sommez; Manning Publications</li> -<li>Search Inside Yourself - The Unexpected path to Achieving Success, Happiness (and World Peace); Chade-Meng Tan, Daniel Goleman, Jon Kabat-Zinn; HarperOne</li> +<li>The Complete Software Developer's Career Guide; John Sonmez; Unabridged Audiobook</li> +<li>Eat That Frog!; Brian Tracy; Hodder Paperbacks</li> <li>The Off Switch; Mark Cropley; Virgin Books (RE-READ 1ST TIME)</li> <li>The Courage to Be Disliked; Ichiro Kishimi and Fumitake Koga; Audiobook</li> -<li>The 7 Habits Of Highly Effective People; Stephen R. Covey; Simon & Schuster UK</li> -<li>Staff Engineer: Leadership beyond the management track; Will Larson; Audiobook</li> +<li>The Phoenix Project - A Novel About IT, DevOps, and Helping your Business Win; Gene Kim and Kevin Behr; Trade Select</li> +<li>The Power of Now; Eckhard Tolle; Yellow Kite</li> +<li>Digital Minimalism; Cal Newport; Portofolio Penguin</li> +<li>101 Essays that change the way you think; Brianna Wiest; Audiobook</li> <li>97 Things Every Engineering Manager Should Know; Camille Fournier; Audiobook</li> -<li>The Good Enough Job; Simone Stolzoff; Ebury Edge</li> -<li>The Software Engineer's Guidebook: Navigating senior, tech lead, and staff engineer positions at tech companies and startups; Gergely Orosz; Audiobook </li> +<li>Who Moved My Cheese?; Dr. Spencer Johnson; Vermilion</li> </ul><br /> <a class='textlink' href='../notes/index.html'>Here are notes of mine for some of the books</a><br /> <br /> @@ -165,22 +165,22 @@ <span>Some of these were in-person with exams; others were online learning lectures only. In random order:</span><br /> <br /> <ul> -<li>Algorithms Video Lectures; Robert Sedgewick; O'Reilly Online</li> -<li>Scripting Vim; Damian Conway; O'Reilly Online</li> -<li>Apache Tomcat Best Practises; 3-day on-site training</li> -<li>MySQL Deep Dive Workshop; 2-day on-site training</li> -<li>The Well-Grounded Rubyist Video Edition; David. A. Black; O'Reilly Online</li> -<li>Linux Security and Isolation APIs Training; Michael Kerrisk; 3-day on-site training</li> +<li>The Ultimate Kubernetes Bootcamp; School of Devops; O'Reilly Online</li> +<li>Red Hat Certified System Administrator; Course + certification (Although I had the option, I decided not to take the next course as it is more effective to self learn what I need)</li> <li>Protocol buffers; O'Reilly Online</li> -<li>F5 Loadbalancers Training; 2-day on-site training; F5, Inc. </li> +<li>Scripting Vim; Damian Conway; O'Reilly Online</li> +<li>Cloud Operations on AWS - Learn how to configure, deploy, maintain, and troubleshoot your AWS environments; 3-day online live training with labs; Amazon</li> <li>Functional programming lecture; Remote University of Hagen</li> +<li>Linux Security and Isolation APIs Training; Michael Kerrisk; 3-day on-site training</li> +<li>Ultimate Go Programming; Bill Kennedy; O'Reilly Online</li> +<li>Apache Tomcat Best Practises; 3-day on-site training</li> <li>Developing IaC with Terraform (with Live Lessons); O'Reilly Online</li> <li>Structure and Interpretation of Computer Programs; Harold Abelson and more...; </li> -<li>The Ultimate Kubernetes Bootcamp; School of Devops; O'Reilly Online</li> -<li>Ultimate Go Programming; Bill Kennedy; O'Reilly Online</li> -<li>Red Hat Certified System Administrator; Course + certification (Although I had the option, I decided not to take the next course as it is more effective to self learn what I need)</li> -<li>Cloud Operations on AWS - Learn how to configure, deploy, maintain, and troubleshoot your AWS environments; 3-day online live training with labs; Amazon</li> +<li>Algorithms Video Lectures; Robert Sedgewick; O'Reilly Online</li> <li>AWS Immersion Day; Amazon; 1-day interactive online training </li> +<li>The Well-Grounded Rubyist Video Edition; David. A. Black; O'Reilly Online</li> +<li>F5 Loadbalancers Training; 2-day on-site training; F5, Inc. </li> +<li>MySQL Deep Dive Workshop; 2-day on-site training</li> </ul><br /> <h2 style='display: inline' id='technical-guides'>Technical guides</h2><br /> <br /> @@ -188,8 +188,8 @@ <br /> <ul> <li>How CPUs work at https://cpu.land</li> -<li>Advanced Bash-Scripting Guide </li> <li>Raku Guide at https://raku.guide </li> +<li>Advanced Bash-Scripting Guide </li> </ul><br /> <h2 style='display: inline' id='podcasts'>Podcasts</h2><br /> <br /> @@ -199,59 +199,59 @@ <br /> <ul> <li>Cup o' Go [Golang]</li> -<li>The Pragmatic Engineer Podcast</li> -<li>BSD Now [BSD]</li> -<li>The ProdCast (Google SRE Podcast)</li> -<li>The Changelog Podcast(s)</li> -<li>Fallthrough [Golang]</li> -<li>Wednesday Wisdom</li> -<li>Hidden Brain</li> <li>Modern Mentor</li> -<li>Maintainable</li> +<li>The ProdCast (Google SRE Podcast)</li> <li>Dev Interrupted</li> -<li>Backend Banter</li> <li>Deep Questions with Cal Newport</li> +<li>BSD Now [BSD]</li> +<li>The Pragmatic Engineer Podcast</li> +<li>Wednesday Wisdom</li> +<li>The Changelog Podcast(s)</li> <li>Fork Around And Find Out</li> +<li>Maintainable</li> <li>Pratical AI</li> +<li>Hidden Brain</li> +<li>Fallthrough [Golang]</li> +<li>Backend Banter</li> </ul><br /> <h3 style='display: inline' id='podcasts-i-liked'>Podcasts I liked</h3><br /> <br /> <span>I liked them but am not listening to them anymore. The podcasts have either "finished" (no more episodes) or I stopped listening to them due to time constraints or a shift in my interests.</span><br /> <br /> <ul> +<li>Modern Mentor</li> +<li>Ship It (predecessor of Fork Around And Find Out)</li> +<li>FLOSS weekly</li> <li>Java Pub House</li> -<li>CRE: Chaosradio Express [german]</li> <li>Go Time (predecessor of fallthrough)</li> -<li>FLOSS weekly</li> -<li>Ship It (predecessor of Fork Around And Find Out)</li> -<li>Modern Mentor</li> +<li>CRE: Chaosradio Express [german]</li> </ul><br /> <h2 style='display: inline' id='newsletters-i-like'>Newsletters I like</h2><br /> <br /> <span>This is a mix of tech and non-tech newsletters I am subscribed to. In random order:</span><br /> <br /> <ul> -<li>Ruby Weekly</li> -<li>Changelog News</li> -<li>byteSizeGo</li> -<li>The Imperfectionist</li> +<li>Applied Go Weekly Newsletter</li> +<li>The Pragmatic Engineer</li> <li>The Valuable Dev</li> -<li>VK Newsletter</li> -<li>Golang Weekly</li> <li>Monospace Mentor</li> -<li>Andreas Brandhorst Newsletter (Sci-Fi author)</li> <li>Register Spill</li> -<li>The Pragmatic Engineer</li> -<li>Applied Go Weekly Newsletter</li> +<li>Golang Weekly</li> +<li>The Imperfectionist</li> +<li>Changelog News</li> +<li>VK Newsletter</li> +<li>Andreas Brandhorst Newsletter (Sci-Fi author)</li> +<li>Ruby Weekly</li> +<li>byteSizeGo</li> </ul><br /> <h2 style='display: inline' id='magazines-i-liked'>Magazines I like(d)</h2><br /> <br /> <span>This is a mix of tech I like(d). I may not be a current subscriber, but now and then, I buy an issue. In random order:</span><br /> <br /> <ul> -<li>freeX (not published anymore)</li> <li>LWN (online only)</li> <li>Linux Magazine</li> +<li>freeX (not published anymore)</li> <li>Linux User</li> </ul><br /> <h2 style='display: inline' id='youtube-channels'>YouTube channels</h2><br /> diff --git a/about/showcase.html b/about/showcase.html index e0d7d57d..6e736a44 100644 --- a/about/showcase.html +++ b/about/showcase.html @@ -13,7 +13,7 @@ </p> <h1 style='display: inline' id='project-showcase'>Project Showcase</h1><br /> <br /> -<span>Generated on: 2026-03-11</span><br /> +<span>Generated on: 2026-03-12</span><br /> <br /> <span>This page showcases my side projects, providing an overview of what each project does, its technical implementation, and key metrics. Each project summary includes information about the programming languages used, development activity, releases, and licensing. The projects are ranked by score, which combines recent activity, project size, tag history, and whether the project has shipped a release.</span><br /> <br /> @@ -25,56 +25,56 @@ <li>⇢ <a href='#projects'>Projects</a></li> <li>⇢ ⇢ <a href='#1-ior-11'>1. ior 1←1</a></li> <li>⇢ ⇢ <a href='#2-timesamurai-2'>2. timesamurai 2</a></li> -<li>⇢ ⇢ <a href='#3-dotfiles-33'>3. dotfiles 3←3</a></li> -<li>⇢ ⇢ <a href='#4-loadbars-447'>4. loadbars 4↖47</a></li> -<li>⇢ ⇢ <a href='#5-foostore-57'>5. foostore 5↖7</a></li> -<li>⇢ ⇢ <a href='#6-epimetheus-64'>6. epimetheus 6↙4</a></li> -<li>⇢ ⇢ <a href='#7-conf-75'>7. conf 7↙5</a></li> -<li>⇢ ⇢ <a href='#8-scifi-88'>8. scifi 8←8</a></li> -<li>⇢ ⇢ <a href='#9-log4jbench-99'>9. log4jbench 9←9</a></li> -<li>⇢ ⇢ <a href='#10-rcm-1010'>10. rcm 10←10</a></li> -<li>⇢ ⇢ <a href='#11-yoga-1112'>11. yoga 11↖12</a></li> -<li>⇢ ⇢ <a href='#12-gogios-1211'>12. gogios 12↙11</a></li> -<li>⇢ ⇢ <a href='#13-totalrecall-1314'>13. totalrecall 13↖14</a></li> -<li>⇢ ⇢ <a href='#14-perc-1413'>14. perc 14↙13</a></li> -<li>⇢ ⇢ <a href='#15-hexai-152'>15. hexai 15↙2</a></li> -<li>⇢ ⇢ <a href='#16-tasksamurai-1616'>16. tasksamurai 16←16</a></li> -<li>⇢ ⇢ <a href='#17-gitsyncer-1715'>17. gitsyncer 17↙15</a></li> +<li>⇢ ⇢ <a href='#3-hexai-32'>3. hexai 3↙2</a></li> +<li>⇢ ⇢ <a href='#4-dotfiles-43'>4. dotfiles 4↙3</a></li> +<li>⇢ ⇢ <a href='#5-loadbars-547'>5. loadbars 5↖47</a></li> +<li>⇢ ⇢ <a href='#6-foostore-67'>6. foostore 6↖7</a></li> +<li>⇢ ⇢ <a href='#7-epimetheus-74'>7. epimetheus 7↙4</a></li> +<li>⇢ ⇢ <a href='#8-conf-85'>8. conf 8↙5</a></li> +<li>⇢ ⇢ <a href='#9-scifi-98'>9. scifi 9↙8</a></li> +<li>⇢ ⇢ <a href='#10-gitsyncer-1015'>10. gitsyncer 10↖15</a></li> +<li>⇢ ⇢ <a href='#11-log4jbench-119'>11. log4jbench 11↙9</a></li> +<li>⇢ ⇢ <a href='#12-rcm-1210'>12. rcm 12↙10</a></li> +<li>⇢ ⇢ <a href='#13-yoga-1312'>13. yoga 13↙12</a></li> +<li>⇢ ⇢ <a href='#14-gogios-1411'>14. gogios 14↙11</a></li> +<li>⇢ ⇢ <a href='#15-totalrecall-1514'>15. totalrecall 15↙14</a></li> +<li>⇢ ⇢ <a href='#16-perc-1613'>16. perc 16↙13</a></li> +<li>⇢ ⇢ <a href='#17-tasksamurai-1716'>17. tasksamurai 17↙16</a></li> <li>⇢ ⇢ <a href='#18-gos-1818'>18. gos 18←18</a></li> <li>⇢ ⇢ <a href='#19-foostats-1917'>19. foostats 19↙17</a></li> <li>⇢ ⇢ <a href='#20-timr-2019'>20. timr 20↙19</a></li> <li>⇢ ⇢ <a href='#21-dtail-2120'>21. dtail 21↙20</a></li> -<li>⇢ ⇢ <a href='#22-gemtexter-2222'>22. gemtexter 22←22</a></li> -<li>⇢ ⇢ <a href='#23-wireguardmeshgenerator-2323'>23. wireguardmeshgenerator 23←23</a></li> +<li>⇢ ⇢ <a href='#22-wireguardmeshgenerator-2223'>22. wireguardmeshgenerator 22↖23</a></li> +<li>⇢ ⇢ <a href='#23-gemtexter-2322'>23. gemtexter 23↙22</a></li> <li>⇢ ⇢ <a href='#24-goprecords-2424'>24. goprecords 24←24</a></li> <li>⇢ ⇢ <a href='#25-ds-sim-2521'>25. ds-sim 25↙21</a></li> <li>⇢ ⇢ <a href='#26-quicklogger-2625'>26. quicklogger 26↙25</a></li> <li>⇢ ⇢ <a href='#27-sillybench-2727'>27. sillybench 27←27</a></li> <li>⇢ ⇢ <a href='#28-terraform-2826'>28. terraform 28↙26</a></li> -<li>⇢ ⇢ <a href='#29-geheim-2930'>29. geheim 29↖30</a></li> -<li>⇢ ⇢ <a href='#30-gorum-3028'>30. gorum 30↙28</a></li> -<li>⇢ ⇢ <a href='#31-docker-radicale-server-3131'>31. docker-radicale-server 31←31</a></li> -<li>⇢ ⇢ <a href='#32-photoalbum-3234'>32. photoalbum 32↖34</a></li> -<li>⇢ ⇢ <a href='#33-randomjournalpage-3333'>33. randomjournalpage 33←33</a></li> -<li>⇢ ⇢ <a href='#34-ioriot-3435'>34. ioriot 34↖35</a></li> -<li>⇢ ⇢ <a href='#35-xerl-3542'>35. xerl 35↖42</a></li> +<li>⇢ ⇢ <a href='#29-guprecords-2929'>29. guprecords 29←29</a></li> +<li>⇢ ⇢ <a href='#30-geheim-3030'>30. geheim 30←30</a></li> +<li>⇢ ⇢ <a href='#31-gorum-3128'>31. gorum 31↙28</a></li> +<li>⇢ ⇢ <a href='#32-docker-radicale-server-3231'>32. docker-radicale-server 32↙31</a></li> +<li>⇢ ⇢ <a href='#33-photoalbum-3334'>33. photoalbum 33↖34</a></li> +<li>⇢ ⇢ <a href='#34-randomjournalpage-3433'>34. randomjournalpage 34↙33</a></li> +<li>⇢ ⇢ <a href='#35-ioriot-3535'>35. ioriot 35←35</a></li> <li>⇢ ⇢ <a href='#36-algorithms-3632'>36. algorithms 36↙32</a></li> <li>⇢ ⇢ <a href='#37-ipv6test-3736'>37. ipv6test 37↙36</a></li> <li>⇢ ⇢ <a href='#38-staticfarm-apache-handlers-3840'>38. staticfarm-apache-handlers 38↖40</a></li> <li>⇢ ⇢ <a href='#39-sway-autorotate-3938'>39. sway-autorotate 39↙38</a></li> -<li>⇢ ⇢ <a href='#40-guprecords-4029'>40. guprecords 40↙29</a></li> -<li>⇢ ⇢ <a href='#41-mon-4139'>41. mon 41↙39</a></li> -<li>⇢ ⇢ <a href='#42-fapi-4244'>42. fapi 42↖44</a></li> -<li>⇢ ⇢ <a href='#43-pingdomfetch-4341'>43. pingdomfetch 43↙41</a></li> -<li>⇢ ⇢ <a href='#44-fype-4437'>44. fype 44↙37</a></li> -<li>⇢ ⇢ <a href='#45-pwgrep-4550'>45. pwgrep 45↖50</a></li> +<li>⇢ ⇢ <a href='#40-mon-4039'>40. mon 40↙39</a></li> +<li>⇢ ⇢ <a href='#41-fapi-4144'>41. fapi 41↖44</a></li> +<li>⇢ ⇢ <a href='#42-pingdomfetch-4241'>42. pingdomfetch 42↙41</a></li> +<li>⇢ ⇢ <a href='#43-fype-4337'>43. fype 43↙37</a></li> +<li>⇢ ⇢ <a href='#44-pwgrep-4450'>44. pwgrep 44↖50</a></li> +<li>⇢ ⇢ <a href='#45-xerl-4542'>45. xerl 45↙42</a></li> <li>⇢ ⇢ <a href='#46-awksite-4661'>46. awksite 46↖61</a></li> <li>⇢ ⇢ <a href='#47-gotop-4748'>47. gotop 47↖48</a></li> <li>⇢ ⇢ <a href='#48-japi-4853'>48. japi 48↖53</a></li> -<li>⇢ ⇢ <a href='#49-rubyfy-4949'>49. rubyfy 49←49</a></li> -<li>⇢ ⇢ <a href='#50-perl-c-fibonacci-5045'>50. perl-c-fibonacci 50↙45</a></li> -<li>⇢ ⇢ <a href='#51-netdiff-5156'>51. netdiff 51↖56</a></li> -<li>⇢ ⇢ <a href='#52-perldaemon-5251'>52. perldaemon 52↙51</a></li> +<li>⇢ ⇢ <a href='#49-perldaemon-4951'>49. perldaemon 49↖51</a></li> +<li>⇢ ⇢ <a href='#50-rubyfy-5049'>50. rubyfy 50↙49</a></li> +<li>⇢ ⇢ <a href='#51-perl-c-fibonacci-5145'>51. perl-c-fibonacci 51↙45</a></li> +<li>⇢ ⇢ <a href='#52-netdiff-5256'>52. netdiff 52↖56</a></li> <li>⇢ ⇢ <a href='#53-jsmstrade-5352'>53. jsmstrade 53↙52</a></li> <li>⇢ ⇢ <a href='#54-muttdelay-5455'>54. muttdelay 54↖55</a></li> <li>⇢ ⇢ <a href='#55-netcalendar-5546'>55. netcalendar 55↙46</a></li> @@ -92,11 +92,11 @@ <br /> <ul> <li>📦 Total Projects: 64</li> -<li>📊 Total Commits: 13,579</li> -<li>📈 Total Lines of Code: 302,300</li> -<li>📄 Total Lines of Documentation: 42,965</li> -<li>💻 Languages: Go (55.1%), Java (13.6%), C (6.4%), YAML (5.2%), HTML (4.9%), Perl (4.2%), Shell (3.0%), C/C++ (1.4%), Ruby (1.0%), Config (1.0%), HCL (0.9%), CSS (0.7%), Python (0.7%), JSON (0.5%), Make (0.4%), XML (0.4%), Haskell (0.2%), JavaScript (0.2%), TOML (0.1%)</li> -<li>📚 Documentation: Markdown (71.5%), Text (27.2%), LaTeX (1.3%)</li> +<li>📊 Total Commits: 13,595</li> +<li>📈 Total Lines of Code: 334,197</li> +<li>📄 Total Lines of Documentation: 46,922</li> +<li>💻 Languages: Go (53.7%), Java (12.3%), C (5.8%), CSS (5.3%), YAML (4.7%), Perl (4.3%), HTML (2.9%), Shell (2.6%), Python (2.1%), C/C++ (1.3%), JSON (1.1%), Config (1.0%), Ruby (0.9%), HCL (0.8%), Make (0.4%), XML (0.2%), Haskell (0.2%), JavaScript (0.1%), Raku (0.1%)</li> +<li>📚 Documentation: Markdown (74.1%), Text (24.7%), LaTeX (1.2%)</li> <li>🚀 Release Status: 42 released, 22 experimental (65.6% with releases, 34.4% experimental)</li> </ul><br /> <h2 style='display: inline' id='projects'>Projects</h2><br /> @@ -111,7 +111,7 @@ <li>📄 Lines of Documentation: 3394</li> <li>🏷️ Tags: 0</li> <li>📅 Development Period: 2024-01-18 to 2026-03-11</li> -<li>🏆 Score: 147.5 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 111.7 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: No license found</li> <li>🧪 Status: Experimental (no releases yet)</li> </ul><br /> @@ -135,7 +135,7 @@ <li>📄 Lines of Documentation: 112</li> <li>🏷️ Tags: 4</li> <li>📅 Development Period: 2025-06-25 to 2026-03-07</li> -<li>🏆 Score: 65.5 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 59.3 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: MIT</li> <li>🏷️ Latest Release: v0.7.0 (2026-03-05)</li> </ul><br /> @@ -147,17 +147,41 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='3-dotfiles-33'>3. dotfiles 3←3</h3><br /> +<h3 style='display: inline' id='3-hexai-32'>3. hexai 3↙2</h3><br /> +<br /> +<ul> +<li>💻 Languages: Go (100.0%)</li> +<li>📚 Documentation: Markdown (100.0%)</li> +<li>📊 Commits: 377</li> +<li>📈 Lines of Code: 31218</li> +<li>📄 Lines of Documentation: 4089</li> +<li>🏷️ Tags: 50</li> +<li>📅 Development Period: 2025-08-01 to 2026-03-10</li> +<li>🏆 Score: 50.6 (combines recent activity, code size, tags, and release status)</li> +<li>⚖️ License: No license found</li> +<li>🏷️ Latest Release: v0.21.0 (2026-02-12)</li> +</ul><br /> +<br /> +<a href='showcase/hexai/image-1.png'><img alt='hexai screenshot' title='hexai screenshot' src='showcase/hexai/image-1.png' /></a><br /> +<br /> +<span>Hexai, the AI addition for your Helix Editor (https://helix-editor.com) .. Other editors should work but weren't tested.</span><br /> +<br /> +<a class='textlink' href='https://codeberg.org/snonux/hexai'>View on Codeberg</a><br /> +<a class='textlink' href='https://github.com/snonux/hexai'>View on GitHub</a><br /> +<br /> +<span>---</span><br /> +<br /> +<h3 style='display: inline' id='4-dotfiles-43'>4. dotfiles 4↙3</h3><br /> <br /> <ul> <li>💻 Languages: Shell (66.6%), CSS (10.9%), Config (10.1%), TOML (10.0%), JSON (1.1%), Ruby (1.0%), INI (0.2%)</li> <li>📚 Documentation: Markdown (100.0%)</li> -<li>📊 Commits: 840</li> +<li>📊 Commits: 845</li> <li>📈 Lines of Code: 2990</li> <li>📄 Lines of Documentation: 5386</li> <li>🏷️ Tags: 0</li> <li>📅 Development Period: 2023-07-30 to 2026-03-10</li> -<li>🏆 Score: 38.5 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 35.1 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: No license found</li> <li>🧪 Status: Experimental (no releases yet)</li> </ul><br /> @@ -169,7 +193,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='4-loadbars-447'>4. loadbars 4↖47</h3><br /> +<h3 style='display: inline' id='5-loadbars-547'>5. loadbars 5↖47</h3><br /> <br /> <ul> <li>💻 Languages: Go (92.8%), Shell (7.2%)</li> @@ -179,7 +203,7 @@ <li>📄 Lines of Documentation: 328</li> <li>🏷️ Tags: 47</li> <li>📅 Development Period: 2010-11-05 to 2026-03-02</li> -<li>🏆 Score: 26.4 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 25.5 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: Custom License</li> <li>🏷️ Latest Release: v0.11.1 (2026-02-17)</li> </ul><br /> @@ -193,7 +217,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='5-foostore-57'>5. foostore 5↖7</h3><br /> +<h3 style='display: inline' id='6-foostore-67'>6. foostore 6↖7</h3><br /> <br /> <ul> <li>💻 Languages: Go (98.4%), Shell (1.6%)</li> @@ -203,7 +227,7 @@ <li>📄 Lines of Documentation: 250</li> <li>🏷️ Tags: 9</li> <li>📅 Development Period: 2018-05-26 to 2026-03-07</li> -<li>🏆 Score: 18.0 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 17.6 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: No license found</li> <li>🏷️ Latest Release: v0.5.3 (2026-03-02)</li> </ul><br /> @@ -215,7 +239,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='6-epimetheus-64'>6. epimetheus 6↙4</h3><br /> +<h3 style='display: inline' id='7-epimetheus-74'>7. epimetheus 7↙4</h3><br /> <br /> <ul> <li>💻 Languages: Go (85.2%), Shell (14.8%)</li> @@ -225,7 +249,7 @@ <li>📄 Lines of Documentation: 1736</li> <li>🏷️ Tags: 0</li> <li>📅 Development Period: 2026-02-07 to 2026-03-07</li> -<li>🏆 Score: 15.0 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 14.5 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: No license found</li> <li>🧪 Status: Experimental (no releases yet)</li> </ul><br /> @@ -239,7 +263,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='7-conf-75'>7. conf 7↙5</h3><br /> +<h3 style='display: inline' id='8-conf-85'>8. conf 8↙5</h3><br /> <br /> <ul> <li>💻 Languages: YAML (80.7%), Perl (9.9%), Shell (6.0%), Python (2.3%), Docker (0.7%), Config (0.2%), HTML (0.1%)</li> @@ -249,7 +273,7 @@ <li>📄 Lines of Documentation: 6572</li> <li>🏷️ Tags: 0</li> <li>📅 Development Period: 2021-12-28 to 2026-02-15</li> -<li>🏆 Score: 11.5 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 11.3 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: No license found</li> <li>🧪 Status: Experimental (no releases yet)</li> </ul><br /> @@ -261,7 +285,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='8-scifi-88'>8. scifi 8←8</h3><br /> +<h3 style='display: inline' id='9-scifi-98'>9. scifi 9↙8</h3><br /> <br /> <ul> <li>💻 Languages: JSON (35.9%), CSS (30.6%), JavaScript (29.6%), HTML (3.8%)</li> @@ -271,7 +295,7 @@ <li>📄 Lines of Documentation: 853</li> <li>🏷️ Tags: 0</li> <li>📅 Development Period: 2026-01-25 to 2026-01-27</li> -<li>🏆 Score: 7.3 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 7.2 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: No license found</li> <li>🧪 Status: Experimental (no releases yet)</li> </ul><br /> @@ -283,7 +307,29 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='9-log4jbench-99'>9. log4jbench 9←9</h3><br /> +<h3 style='display: inline' id='10-gitsyncer-1015'>10. gitsyncer 10↖15</h3><br /> +<br /> +<ul> +<li>💻 Languages: Go (93.4%), Shell (6.3%), JSON (0.3%)</li> +<li>📚 Documentation: Markdown (100.0%)</li> +<li>📊 Commits: 132</li> +<li>📈 Lines of Code: 11821</li> +<li>📄 Lines of Documentation: 2456</li> +<li>🏷️ Tags: 34</li> +<li>📅 Development Period: 2025-06-23 to 2026-03-11</li> +<li>🏆 Score: 5.5 (combines recent activity, code size, tags, and release status)</li> +<li>⚖️ License: BSD-2-Clause</li> +<li>🏷️ Latest Release: v0.15.4 (2026-03-11)</li> +</ul><br /> +<br /> +<span>GitSyncer is a tool for synchronizing git repositories between multiple organizations (e.g., GitHub and Codeberg). It automatically keeps all branches in sync across different git hosting platforms.</span><br /> +<br /> +<a class='textlink' href='https://codeberg.org/snonux/gitsyncer'>View on Codeberg</a><br /> +<a class='textlink' href='https://github.com/snonux/gitsyncer'>View on GitHub</a><br /> +<br /> +<span>---</span><br /> +<br /> +<h3 style='display: inline' id='11-log4jbench-119'>11. log4jbench 11↙9</h3><br /> <br /> <ul> <li>💻 Languages: Java (78.9%), XML (21.1%)</li> @@ -305,7 +351,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='10-rcm-1010'>10. rcm 10←10</h3><br /> +<h3 style='display: inline' id='12-rcm-1210'>12. rcm 12↙10</h3><br /> <br /> <ul> <li>💻 Languages: Ruby (99.6%), TOML (0.4%)</li> @@ -329,7 +375,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='11-yoga-1112'>11. yoga 11↖12</h3><br /> +<h3 style='display: inline' id='13-yoga-1312'>13. yoga 13↙12</h3><br /> <br /> <ul> <li>💻 Languages: Go (69.1%), HTML (30.9%)</li> @@ -339,7 +385,7 @@ <li>📄 Lines of Documentation: 196</li> <li>🏷️ Tags: 9</li> <li>📅 Development Period: 2025-10-01 to 2026-03-07</li> -<li>🏆 Score: 4.9 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 4.8 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: No license found</li> <li>🏷️ Latest Release: v0.4.0 (2026-01-28)</li> </ul><br /> @@ -353,7 +399,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='12-gogios-1211'>12. gogios 12↙11</h3><br /> +<h3 style='display: inline' id='14-gogios-1411'>14. gogios 14↙11</h3><br /> <br /> <ul> <li>💻 Languages: Go (98.9%), JSON (0.6%), YAML (0.5%)</li> @@ -363,7 +409,7 @@ <li>📄 Lines of Documentation: 394</li> <li>🏷️ Tags: 10</li> <li>📅 Development Period: 2023-04-17 to 2026-02-16</li> -<li>🏆 Score: 4.8 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 4.7 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: Custom License</li> <li>🏷️ Latest Release: v1.4.1 (2026-02-16)</li> </ul><br /> @@ -377,7 +423,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='13-totalrecall-1314'>13. totalrecall 13↖14</h3><br /> +<h3 style='display: inline' id='15-totalrecall-1514'>15. totalrecall 15↙14</h3><br /> <br /> <ul> <li>💻 Languages: Go (99.0%), Shell (0.5%), YAML (0.4%)</li> @@ -401,7 +447,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='14-perc-1413'>14. perc 14↙13</h3><br /> +<h3 style='display: inline' id='16-perc-1613'>16. perc 16↙13</h3><br /> <br /> <ul> <li>💻 Languages: Go (100.0%)</li> @@ -423,31 +469,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='15-hexai-152'>15. hexai 15↙2</h3><br /> -<br /> -<ul> -<li>💻 Languages: Go (66.1%), HTML (33.9%)</li> -<li>📚 Documentation: Markdown (100.0%)</li> -<li>📊 Commits: 377</li> -<li>📈 Lines of Code: 28986</li> -<li>📄 Lines of Documentation: 561</li> -<li>🏷️ Tags: 50</li> -<li>📅 Development Period: 2025-08-01 to 2026-01-30</li> -<li>🏆 Score: 4.3 (combines recent activity, code size, tags, and release status)</li> -<li>⚖️ License: No license found</li> -<li>🏷️ Latest Release: v0.21.0 (2026-02-12)</li> -</ul><br /> -<br /> -<a href='showcase/hexai/image-1.png'><img alt='hexai screenshot' title='hexai screenshot' src='showcase/hexai/image-1.png' /></a><br /> -<br /> -<span>Hexai, the AI addition for your Helix Editor (https://helix-editor.com) .. Other editors should work but weren't tested.</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/hexai'>View on Codeberg</a><br /> -<a class='textlink' href='https://github.com/snonux/hexai'>View on GitHub</a><br /> -<br /> -<span>---</span><br /> -<br /> -<h3 style='display: inline' id='16-tasksamurai-1616'>16. tasksamurai 16←16</h3><br /> +<h3 style='display: inline' id='17-tasksamurai-1716'>17. tasksamurai 17↙16</h3><br /> <br /> <ul> <li>💻 Languages: Go (99.8%), YAML (0.2%)</li> @@ -471,32 +493,10 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='17-gitsyncer-1715'>17. gitsyncer 17↙15</h3><br /> -<br /> -<ul> -<li>💻 Languages: Go (92.9%), Shell (6.8%), JSON (0.4%)</li> -<li>📚 Documentation: Markdown (100.0%)</li> -<li>📊 Commits: 121</li> -<li>📈 Lines of Code: 10986</li> -<li>📄 Lines of Documentation: 2450</li> -<li>🏷️ Tags: 29</li> -<li>📅 Development Period: 2025-06-23 to 2026-03-11</li> -<li>🏆 Score: 3.6 (combines recent activity, code size, tags, and release status)</li> -<li>⚖️ License: BSD-2-Clause</li> -<li>🏷️ Latest Release: v0.14.0 (2026-03-11)</li> -</ul><br /> -<br /> -<span>GitSyncer is a tool for synchronizing git repositories between multiple organizations (e.g., GitHub and Codeberg). It automatically keeps all branches in sync across different git hosting platforms.</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/gitsyncer'>View on Codeberg</a><br /> -<a class='textlink' href='https://github.com/snonux/gitsyncer'>View on GitHub</a><br /> -<br /> -<span>---</span><br /> -<br /> <h3 style='display: inline' id='18-gos-1818'>18. gos 18←18</h3><br /> <br /> <ul> -<li>💻 Languages: Go (99.5%), JSON (0.2%), Shell (0.2%)</li> +<li>💻 Languages: Go (99.5%), Shell (0.2%), JSON (0.2%)</li> <li>📚 Documentation: Markdown (100.0%)</li> <li>📊 Commits: 402</li> <li>📈 Lines of Code: 4143</li> @@ -527,7 +527,7 @@ <li>📄 Lines of Documentation: 423</li> <li>🏷️ Tags: 2</li> <li>📅 Development Period: 2023-01-02 to 2025-11-01</li> -<li>🏆 Score: 2.5 (combines recent activity, code size, tags, and release status)</li> +<li>🏆 Score: 2.4 (combines recent activity, code size, tags, and release status)</li> <li>⚖️ License: Custom License</li> <li>🏷️ Latest Release: v0.2.0 (2025-10-21)</li> </ul><br /> @@ -585,29 +585,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='22-gemtexter-2222'>22. gemtexter 22←22</h3><br /> -<br /> -<ul> -<li>💻 Languages: Shell (70.8%), CSS (26.2%), Config (1.8%), HTML (1.2%)</li> -<li>📚 Documentation: Text (76.1%), Markdown (23.9%)</li> -<li>📊 Commits: 480</li> -<li>📈 Lines of Code: 2489</li> -<li>📄 Lines of Documentation: 1180</li> -<li>🏷️ Tags: 6</li> -<li>📅 Development Period: 2021-05-21 to 2026-03-08</li> -<li>🏆 Score: 2.0 (combines recent activity, code size, tags, and release status)</li> -<li>⚖️ License: GPL-3.0</li> -<li>🏷️ Latest Release: 3.0.0 (2024-10-01)</li> -</ul><br /> -<br /> -<span>This is the source code of my personal internet site and blog engine. All content is written in Gemini Gemtext format, but the script <span class='inlinecode'>gemtexter</span> generates multiple other static output formats (with zero JavaScript) from it. You can reach the site(s)...</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/gemtexter'>View on Codeberg</a><br /> -<a class='textlink' href='https://github.com/snonux/gemtexter'>View on GitHub</a><br /> -<br /> -<span>---</span><br /> -<br /> -<h3 style='display: inline' id='23-wireguardmeshgenerator-2323'>23. wireguardmeshgenerator 23←23</h3><br /> +<h3 style='display: inline' id='22-wireguardmeshgenerator-2223'>22. wireguardmeshgenerator 22↖23</h3><br /> <br /> <ul> <li>💻 Languages: Ruby (65.4%), YAML (34.6%)</li> @@ -629,6 +607,28 @@ <br /> <span>---</span><br /> <br /> +<h3 style='display: inline' id='23-gemtexter-2322'>23. gemtexter 23↙22</h3><br /> +<br /> +<ul> +<li>💻 Languages: CSS (55.3%), Python (16.1%), HTML (15.3%), JSON (6.6%), Shell (5.3%), Config (1.5%)</li> +<li>📚 Documentation: Text (70.2%), Markdown (29.8%)</li> +<li>📊 Commits: 480</li> +<li>📈 Lines of Code: 30319</li> +<li>📄 Lines of Documentation: 1280</li> +<li>🏷️ Tags: 6</li> +<li>📅 Development Period: 2021-05-21 to 2025-06-22</li> +<li>🏆 Score: 1.4 (combines recent activity, code size, tags, and release status)</li> +<li>⚖️ License: GPL-3.0</li> +<li>🏷️ Latest Release: 3.0.0 (2024-10-01)</li> +</ul><br /> +<br /> +<span>This is the source code of my personal internet site and blog engine. All content is written in Gemini Gemtext format, but the script <span class='inlinecode'>gemtexter</span> generates multiple other static output formats (with zero JavaScript) from it. You can reach the site(s)...</span><br /> +<br /> +<a class='textlink' href='https://codeberg.org/snonux/gemtexter'>View on Codeberg</a><br /> +<a class='textlink' href='https://github.com/snonux/gemtexter'>View on GitHub</a><br /> +<br /> +<span>---</span><br /> +<br /> <h3 style='display: inline' id='24-goprecords-2424'>24. goprecords 24←24</h3><br /> <br /> <ul> @@ -743,7 +743,29 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='29-geheim-2930'>29. geheim 29↖30</h3><br /> +<h3 style='display: inline' id='29-guprecords-2929'>29. guprecords 29←29</h3><br /> +<br /> +<ul> +<li>💻 Languages: Raku (100.0%)</li> +<li>📚 Documentation: Markdown (100.0%)</li> +<li>📊 Commits: 97</li> +<li>📈 Lines of Code: 383</li> +<li>📄 Lines of Documentation: 425</li> +<li>🏷️ Tags: 1</li> +<li>📅 Development Period: 2013-03-22 to 2026-03-07</li> +<li>🏆 Score: 0.5 (combines recent activity, code size, tags, and release status)</li> +<li>⚖️ License: No license found</li> +<li>🏷️ Latest Release: v1.0.0 (2023-04-29)</li> +</ul><br /> +<br /> +<span>guprecords: source code repository.</span><br /> +<br /> +<a class='textlink' href='https://codeberg.org/snonux/guprecords'>View on Codeberg</a><br /> +<a class='textlink' href='https://github.com/snonux/guprecords'>View on GitHub</a><br /> +<br /> +<span>---</span><br /> +<br /> +<h3 style='display: inline' id='30-geheim-3030'>30. geheim 30←30</h3><br /> <br /> <ul> <li>💻 Languages: Ruby (86.7%), Shell (13.3%)</li> @@ -765,7 +787,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='30-gorum-3028'>30. gorum 30↙28</h3><br /> +<h3 style='display: inline' id='31-gorum-3128'>31. gorum 31↙28</h3><br /> <br /> <ul> <li>💻 Languages: Go (91.3%), JSON (6.4%), YAML (2.3%)</li> @@ -787,7 +809,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='31-docker-radicale-server-3131'>31. docker-radicale-server 31←31</h3><br /> +<h3 style='display: inline' id='32-docker-radicale-server-3231'>32. docker-radicale-server 32↙31</h3><br /> <br /> <ul> <li>💻 Languages: Make (57.5%), Docker (42.5%)</li> @@ -809,7 +831,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='32-photoalbum-3234'>32. photoalbum 32↖34</h3><br /> +<h3 style='display: inline' id='33-photoalbum-3334'>33. photoalbum 33↖34</h3><br /> <br /> <ul> <li>💻 Languages: Shell (80.1%), Make (12.3%), Config (7.6%)</li> @@ -833,7 +855,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='33-randomjournalpage-3333'>33. randomjournalpage 33←33</h3><br /> +<h3 style='display: inline' id='34-randomjournalpage-3433'>34. randomjournalpage 34↙33</h3><br /> <br /> <ul> <li>💻 Languages: Shell (94.1%), Make (5.9%)</li> @@ -856,7 +878,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='34-ioriot-3435'>34. ioriot 34↖35</h3><br /> +<h3 style='display: inline' id='35-ioriot-3535'>35. ioriot 35←35</h3><br /> <br /> <ul> <li>💻 Languages: C (55.5%), C/C++ (24.0%), Config (19.6%), Make (1.0%)</li> @@ -881,29 +903,6 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='35-xerl-3542'>35. xerl 35↖42</h3><br /> -<br /> -<ul> -<li>💻 Languages: CSS (54.6%), XML (39.1%), Perl (4.0%), Make (2.2%)</li> -<li>📚 Documentation: Text (91.2%), Org (4.9%), Markdown (3.9%)</li> -<li>📊 Commits: 671</li> -<li>📈 Lines of Code: 815</li> -<li>📄 Lines of Documentation: 102</li> -<li>🏷️ Tags: 1</li> -<li>📅 Development Period: 2011-03-06 to 2021-11-02</li> -<li>🏆 Score: 0.2 (combines recent activity, code size, tags, and release status)</li> -<li>⚖️ License: Custom License</li> -<li>🏷️ Latest Release: v1.0.0 (2018-12-22)</li> -</ul><br /> -<span>⚠️ **Notice**: This project appears to be finished, obsolete, or no longer maintained. Last meaningful activity was over 2 years ago. Use at your own risk.</span><br /> -<br /> -<span>Those are the host templates to be used with Xerl itself.</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/xerl'>View on Codeberg</a><br /> -<a class='textlink' href='https://github.com/snonux/xerl'>View on GitHub</a><br /> -<br /> -<span>---</span><br /> -<br /> <h3 style='display: inline' id='36-algorithms-3632'>36. algorithms 36↙32</h3><br /> <br /> <ul> @@ -995,28 +994,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='40-guprecords-4029'>40. guprecords 40↙29</h3><br /> -<br /> -<ul> -<li>💻 Languages: Raku (100.0%)</li> -<li>📊 Commits: 97</li> -<li>📈 Lines of Code: 195</li> -<li>🏷️ Tags: 1</li> -<li>📅 Development Period: 2013-03-22 to 2023-03-09</li> -<li>🏆 Score: 0.2 (combines recent activity, code size, tags, and release status)</li> -<li>⚖️ License: No license found</li> -<li>🏷️ Latest Release: v1.0.0 (2023-04-29)</li> -</ul><br /> -<span>⚠️ **Notice**: This project appears to be finished, obsolete, or no longer maintained. Last meaningful activity was over 2 years ago. Use at your own risk.</span><br /> -<br /> -<span>guprecords: source code repository.</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/guprecords'>View on Codeberg</a><br /> -<a class='textlink' href='https://github.com/snonux/guprecords'>View on GitHub</a><br /> -<br /> -<span>---</span><br /> -<br /> -<h3 style='display: inline' id='41-mon-4139'>41. mon 41↙39</h3><br /> +<h3 style='display: inline' id='40-mon-4039'>40. mon 40↙39</h3><br /> <br /> <ul> <li>💻 Languages: Perl (96.5%), Shell (1.8%), Make (1.2%), Config (0.4%)</li> @@ -1040,7 +1018,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='42-fapi-4244'>42. fapi 42↖44</h3><br /> +<h3 style='display: inline' id='41-fapi-4144'>41. fapi 41↖44</h3><br /> <br /> <ul> <li>💻 Languages: Python (96.6%), Make (3.1%), Config (0.3%)</li> @@ -1064,7 +1042,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='43-pingdomfetch-4341'>43. pingdomfetch 43↙41</h3><br /> +<h3 style='display: inline' id='42-pingdomfetch-4241'>42. pingdomfetch 42↙41</h3><br /> <br /> <ul> <li>💻 Languages: Perl (97.3%), Make (2.7%)</li> @@ -1088,7 +1066,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='44-fype-4437'>44. fype 44↙37</h3><br /> +<h3 style='display: inline' id='43-fype-4337'>43. fype 43↙37</h3><br /> <br /> <ul> <li>💻 Languages: C (77.3%), C/C++ (13.1%), HTML (7.5%), Make (2.1%)</li> @@ -1110,7 +1088,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='45-pwgrep-4550'>45. pwgrep 45↖50</h3><br /> +<h3 style='display: inline' id='44-pwgrep-4450'>44. pwgrep 44↖50</h3><br /> <br /> <ul> <li>💻 Languages: Shell (85.0%), Make (15.0%)</li> @@ -1132,6 +1110,26 @@ <br /> <span>---</span><br /> <br /> +<h3 style='display: inline' id='45-xerl-4542'>45. xerl 45↙42</h3><br /> +<br /> +<ul> +<li>💻 Languages: Perl (98.3%), Config (1.2%), Make (0.5%)</li> +<li>📊 Commits: 671</li> +<li>📈 Lines of Code: 1675</li> +<li>🏷️ Tags: 1</li> +<li>📅 Development Period: 2011-03-06 to 2026-03-07</li> +<li>🏆 Score: 0.1 (combines recent activity, code size, tags, and release status)</li> +<li>⚖️ License: Custom License</li> +<li>🏷️ Latest Release: v1.0.0 (2018-12-22)</li> +</ul><br /> +<br /> +<span>Those are the host templates to be used with Xerl itself.</span><br /> +<br /> +<a class='textlink' href='https://codeberg.org/snonux/xerl'>View on Codeberg</a><br /> +<a class='textlink' href='https://github.com/snonux/xerl'>View on GitHub</a><br /> +<br /> +<span>---</span><br /> +<br /> <h3 style='display: inline' id='46-awksite-4661'>46. awksite 46↖61</h3><br /> <br /> <ul> @@ -1200,7 +1198,28 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='49-rubyfy-4949'>49. rubyfy 49←49</h3><br /> +<h3 style='display: inline' id='49-perldaemon-4951'>49. perldaemon 49↖51</h3><br /> +<br /> +<ul> +<li>💻 Languages: Perl (72.7%), Shell (23.9%), Config (3.4%)</li> +<li>📊 Commits: 111</li> +<li>📈 Lines of Code: 611</li> +<li>🏷️ Tags: 6</li> +<li>📅 Development Period: 2011-02-05 to 2026-03-07</li> +<li>🏆 Score: 0.1 (combines recent activity, code size, tags, and release status)</li> +<li>⚖️ License: Custom License</li> +<li>🏷️ Latest Release: v1.4 (2022-04-29)</li> +</ul><br /> +<br /> +<span>PerlDaemon is a minimal daemon for Linux and other UNIX a like operating system</span><br /> +<span>programmed in Perl. It can be extended to fit any task...</span><br /> +<br /> +<a class='textlink' href='https://codeberg.org/snonux/perldaemon'>View on Codeberg</a><br /> +<a class='textlink' href='https://github.com/snonux/perldaemon'>View on GitHub</a><br /> +<br /> +<span>---</span><br /> +<br /> +<h3 style='display: inline' id='50-rubyfy-5049'>50. rubyfy 50↙49</h3><br /> <br /> <ul> <li>💻 Languages: Ruby (98.5%), JSON (1.5%)</li> @@ -1222,7 +1241,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='50-perl-c-fibonacci-5045'>50. perl-c-fibonacci 50↙45</h3><br /> +<h3 style='display: inline' id='51-perl-c-fibonacci-5145'>51. perl-c-fibonacci 51↙45</h3><br /> <br /> <ul> <li>💻 Languages: C (80.4%), Make (19.6%)</li> @@ -1245,7 +1264,7 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='51-netdiff-5156'>51. netdiff 51↖56</h3><br /> +<h3 style='display: inline' id='52-netdiff-5256'>52. netdiff 52↖56</h3><br /> <br /> <ul> <li>💻 Languages: Shell (52.2%), Make (46.3%), Config (1.5%)</li> @@ -1269,28 +1288,6 @@ <br /> <span>---</span><br /> <br /> -<h3 style='display: inline' id='52-perldaemon-5251'>52. perldaemon 52↙51</h3><br /> -<br /> -<ul> -<li>💻 Languages: Perl (74.2%), Shell (22.2%), Config (3.6%)</li> -<li>📊 Commits: 111</li> -<li>📈 Lines of Code: 659</li> -<li>🏷️ Tags: 6</li> -<li>📅 Development Period: 2011-02-05 to 2022-04-21</li> -<li>🏆 Score: 0.1 (combines recent activity, code size, tags, and release status)</li> -<li>⚖️ License: Custom License</li> -<li>🏷️ Latest Release: v1.4 (2022-04-29)</li> -</ul><br /> -<span>⚠️ **Notice**: This project appears to be finished, obsolete, or no longer maintained. Last meaningful activity was over 2 years ago. Use at your own risk.</span><br /> -<br /> -<span>PerlDaemon is a minimal daemon for Linux and other UNIX a like operating system</span><br /> -<span>programmed in Perl. It can be extended to fit any task...</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/perldaemon'>View on Codeberg</a><br /> -<a class='textlink' href='https://github.com/snonux/perldaemon'>View on GitHub</a><br /> -<br /> -<span>---</span><br /> -<br /> <h3 style='display: inline' id='53-jsmstrade-5352'>53. jsmstrade 53↙52</h3><br /> <br /> <ul> diff --git a/about/showcase/debroid/image-1.png b/about/showcase/debroid/image-1.png index cea2fea5..a25810f8 100644 --- a/about/showcase/debroid/image-1.png +++ b/about/showcase/debroid/image-1.png @@ -46,15 +46,15 @@ <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-primitives-6da842159062d25e.css" /> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-9e07ff8eaaaff3a3.css" /> - <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-a469e846088cc1bf.css" /> + <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-87aa887446e37f5c.css" /> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/github-6e7c458caf1e80bb.css" /> - <script type="application/json" id="client-env">{"locale":"en","featureFlags":["a11y_status_checks_ruleset","actions_custom_images_public_preview_visibility","actions_custom_images_storage_billing_ui_visibility","actions_image_version_event","agent_session_retry_fetch_capi_on_401","alternate_user_config_repo","arianotify_comprehensive_migration","batch_suggested_changes","cache_issue_labels","codespaces_prebuild_region_target_update","coding_agent_model_selection","coding_agent_model_selection_all_skus","contentful_primer_code_blocks","copilot_3p_agent_hovercards","copilot_agent_snippy","copilot_agent_task_list_v2","copilot_agent_tasks_btn_code_nav","copilot_agent_tasks_btn_code_view","copilot_agent_tasks_btn_code_view_lines","copilot_agent_tasks_btn_repo","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_repository_picker","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_cli_install_cta","copilot_code_review_batch_apply_suggestions","copilot_coding_agent_task_response","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_features_zed_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_embedded","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_task_alive_updates","copilot_mission_control_use_task_name","copilot_org_policy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","custom_properties_consolidate_default_value_input","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","disable_soft_navigate_turbo_visit","flex_cta_groups_mvp","global_account_switch_dialog_lazy_load","global_agents_menu_lazy_load","global_create_menu_lazy_load","global_nav_menu_lazy_load","global_nav_react","global_user_menu_lazy_load","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","initial_per_page_pagination_updates","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_fields_global_search","issue_fields_timeline_events","issues_cca_assign_actor_with_agent","issues_dashboard_inp_optimization","issues_dashboard_semantic_search","issues_diff_based_label_updates","issues_expanded_file_types","issues_index_semantic_search","issues_lazy_load_comment_box_suggestions","issues_react_auto_retry_on_error","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_hot_cache","issues_react_low_quality_comment_warning","issues_react_prohibit_title_fallback","issues_react_safari_scroll_preservation","issues_react_use_turbo_for_cross_repo_navigation","issues_service_worker","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_grouped_by_edit_route","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","merge_status_header_feedback","mission_control_retry_on_401","mission_control_use_body_html","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","open_agent_session_in_vscode_insiders","open_agent_session_in_vscode_stable","primer_react_css_has_selector_perf","primer_react_spinner_synchronize_animations","prs_conversations_react","prx_merge_status_button_alt_logic","ruleset_deletion_confirmation","sample_network_conn_type","session_logs_ungroup_reasoning_text","site_calculator_actions_2025","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","suppress_non_representative_vitals","viewscreen_sandbox","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"}</script> + <script type="application/json" id="client-env">{"locale":"en","featureFlags":["a11y_status_checks_ruleset","actions_custom_images_public_preview_visibility","actions_custom_images_storage_billing_ui_visibility","actions_image_version_event","alternate_user_config_repo","arianotify_comprehensive_migration","batch_suggested_changes","codespaces_prebuild_region_target_update","coding_agent_model_selection","coding_agent_model_selection_all_skus","contentful_primer_code_blocks","copilot_3p_agent_hovercards","copilot_agent_snippy","copilot_agent_task_list_v2","copilot_agent_tasks_btn_code_nav","copilot_agent_tasks_btn_code_view","copilot_agent_tasks_btn_code_view_lines","copilot_agent_tasks_btn_repo","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_repository_picker","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_cli_install_cta","copilot_code_review_batch_apply_suggestions","copilot_coding_agent_task_response","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_features_zed_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_embedded","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_task_alive_updates","copilot_mission_control_use_task_name","copilot_org_policy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","custom_properties_consolidate_default_value_input","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","disable_soft_navigate_turbo_visit","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","initial_per_page_pagination_updates","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_fields_global_search","issue_fields_timeline_events","issues_cca_assign_actor_with_agent","issues_dashboard_inp_optimization","issues_dashboard_semantic_search","issues_diff_based_label_updates","issues_expanded_file_types","issues_index_semantic_search","issues_lazy_load_comment_box_suggestions","issues_react_auto_retry_on_error","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_hot_cache","issues_react_low_quality_comment_warning","issues_react_prohibit_title_fallback","issues_react_use_turbo_for_cross_repo_navigation","issues_service_worker","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_grouped_by_edit_route","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","merge_status_header_feedback","mission_control_retry_on_401","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","open_agent_session_in_vscode_insiders","open_agent_session_in_vscode_stable","primer_react_css_has_selector_perf","primer_react_spinner_synchronize_animations","prs_conversations_react","prx_merge_status_button_alt_logic","react_start_transition_hydration","ruleset_deletion_confirmation","sample_network_conn_type","session_logs_ungroup_reasoning_text","site_calculator_actions_2025","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","suppress_non_representative_vitals","viewscreen_sandbox","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"}</script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/high-contrast-cookie-fed1d93364101384.js"></script> -<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/wp-runtime-6cdb06729531c83e.js" defer="defer"></script> +<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/wp-runtime-3d0052a9e3659fdc.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/28839-734cb6d8a7150172.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/85924-e131bec5f99667e1.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/34646-e68f26aaba7f2b0d.js" defer="defer"></script> @@ -69,8 +69,8 @@ <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/46740-6606b1026a237412.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/81751-2c06efb98b9460b1.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/github-elements-21691d9353073fe5.js" defer="defer"></script> -<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/element-registry-c050bda0a9ab04a2.js" defer="defer"></script> -<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/react-core-017b3a88b81253a9.js" defer="defer"></script> +<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/element-registry-98a4a3b1a1e97735.js" defer="defer"></script> +<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/react-core-edbb0c9bf84d212e.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/react-lib-a4cf89fce9a1300a.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/29434-47789cf97f381365.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/79039-2565b539a6ebc09b.js" defer="defer"></script> @@ -80,7 +80,7 @@ <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/62249-6514db80cf0cc26f.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/54195-ca6526f3483e6a6c.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/62988-af4811a8532026f8.js" defer="defer"></script> -<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/60481-307c2e8794863c2f.js" defer="defer"></script> +<script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/60481-3c7a0fd2336c8106.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/46287-7b500b5354119a88.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/2498-7a413c8209222158.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/61075-6d5c1005b38602d6.js" defer="defer"></script> @@ -88,7 +88,7 @@ <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/49029-6c429c0e1bbd1e79.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/99328-5e06da57c4622e21.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/behaviors-7320c3a05a5a8a94.js" defer="defer"></script> -<link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/react-core.36c3e8b046fb98c7.module.css" /> +<link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/react-core.54426a0573bf33a4.module.css" /> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/38302-841b0fa97d5950a1.js" defer="defer"></script> <script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/notifications-global-3413132b6df94f22.js" defer="defer"></script> @@ -100,13 +100,13 @@ <meta name="route-pattern" content="/:user_id/:repository/blob/*name(/*path)" data-turbo-transient> <meta name="route-controller" content="blob" data-turbo-transient> <meta name="route-action" content="show" data-turbo-transient> - <meta name="fetch-nonce" content="v2:fe561ba2-7df5-dd5a-fbaf-55a6d087dd4d"> + <meta name="fetch-nonce" content="v2:3d42a68d-64e8-cf22-ec4b-7167ee089041"> <meta name="current-catalog-service-hash" content="f3abb0cc802f3d7b95fc8762b94bdcb13bf39634c40c357301c4aa1d67a256fb"> - <meta name="request-id" content="E58A:18AAA9:4433DFD:341AD62:69B1D33D" data-pjax-transient="true"/><meta name="html-safe-nonce" content="0bcb208785bed1c0827476fe626bb8525b5a70db8af46a0e68ba547fef8d7d3c" data-pjax-transient="true"/><meta name="visitor-payload" content="eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFNThBOjE4QUFBOTo0NDMzREZEOjM0MUFENjI6NjlCMUQzM0QiLCJ2aXNpdG9yX2lkIjoiMzg0NDI0Mzk2MzYyMzEwMTI0NSIsInJlZ2lvbl9lZGdlIjoiZnJhIiwicmVnaW9uX3JlbmRlciI6ImZyYSJ9" data-pjax-transient="true"/><meta name="visitor-hmac" content="4a310da5bee80c04df7c2da662672112e176cc3b3a15ddd1789e0451439ec324" data-pjax-transient="true"/> + <meta name="request-id" content="AFEC:258656:A1A1DEF:7DC0E19:69B2FF3E" data-pjax-transient="true"/><meta name="html-safe-nonce" content="0cd18f3767913367b1ade0e4ecb33f5fc4966cdb84e66c4c0ff1e63c9c278bae" data-pjax-transient="true"/><meta name="visitor-payload" content="eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBRkVDOjI1ODY1NjpBMUExREVGOjdEQzBFMTk6NjlCMkZGM0UiLCJ2aXNpdG9yX2lkIjoiMzQxMjk0MTAyODQ3NjUxODIwNiIsInJlZ2lvbl9lZGdlIjoiZnJhIiwicmVnaW9uX3JlbmRlciI6ImZyYSJ9" data-pjax-transient="true"/><meta name="visitor-hmac" content="46cc6c94960db57296d426aa32f6d7ead0bb7658d0c833824b1f2b638743da6b" data-pjax-transient="true"/> @@ -186,10 +186,10 @@ <meta name="expected-hostname" content="github.com"> - <meta http-equiv="x-pjax-version" content="1c595b3056aa668aa6cc3cd72dde43889ddcbab4d27d2fbbf6ec7e97d90ba90b" data-turbo-track="reload"> + <meta http-equiv="x-pjax-version" content="d26ff50652ad422ac8bbc550836c8a7f6fabfbd2b890f880a319b25a0c93e47a" data-turbo-track="reload"> <meta http-equiv="x-pjax-csp-version" content="568c098497d98702bac1642a2a853732a047a6ced28eabd3e15d50041a890235" data-turbo-track="reload"> - <meta http-equiv="x-pjax-css-version" content="0d37fe739bdacf25c61c345a5c0eb2ab3b798040f18b42de20917981186c80ae" data-turbo-track="reload"> - <meta http-equiv="x-pjax-js-version" content="a4f3cd270dfe08279eefda2909de042c4590a7bb8184074839398b29bb281e93" data-turbo-track="reload"> + <meta http-equiv="x-pjax-css-version" content="29efb9e0d90eec5f00934d16427f39c7d64ef9893cd48ec5de0eea0e48234ad7" data-turbo-track="reload"> + <meta http-equiv="x-pjax-js-version" content="af9e59253a93f21d4c3264d041c3aac63932d734d4d96de24551038662e1014d" data-turbo-track="reload"> <meta name="turbo-cache-control" content="no-preview" data-turbo-transient=""> @@ -212,7 +212,7 @@ <meta name="browser-errors-url" content="https://api.github.com/_private/browser/errors"> - <meta name="release" content="b5d1be1de21e124553a946d3aca62f6be1736995"> + <meta name="release" content="7547009f927282018375e0256ca6f507071b90a2"> <meta name="ui-target" content="full"> <link rel="mask-icon" href="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" color="#000000"> @@ -312,10 +312,10 @@ </a> <div class="AppHeader-appearanceSettings"> <react-partial-anchor> - <button data-target="react-partial-anchor.anchor" id="icon-button-feca8c33-1451-4ed5-9a42-b6e9dd3ae8c3" aria-labelledby="tooltip-734a0566-11be-473a-8066-64f19cd60d1b" type="button" disabled="disabled" data-view-component="true" class="Button Button--iconOnly Button--invisible Button--medium AppHeader-button HeaderMenu-link border cursor-wait"> <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-sliders Button-visual"> + <button data-target="react-partial-anchor.anchor" id="icon-button-4590ff08-b106-405f-a010-79edd4a87cba" aria-labelledby="tooltip-7539da9d-7227-4681-99e3-44b606ddc1e6" type="button" disabled="disabled" data-view-component="true" class="Button Button--iconOnly Button--invisible Button--medium AppHeader-button HeaderMenu-link border cursor-wait"> <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-sliders Button-visual"> <path d="M15 2.75a.75.75 0 0 1-.75.75h-4a.75.75 0 0 1 0-1.5h4a.75.75 0 0 1 .75.75Zm-8.5.75v1.25a.75.75 0 0 0 1.5 0v-4a.75.75 0 0 0-1.5 0V2H1.75a.75.75 0 0 0 0 1.5H6.5Zm1.25 5.25a.75.75 0 0 0 0-1.5h-6a.75.75 0 0 0 0 1.5h6ZM15 8a.75.75 0 0 1-.75.75H11.5V10a.75.75 0 1 1-1.5 0V6a.75.75 0 0 1 1.5 0v1.25h2.75A.75.75 0 0 1 15 8Zm-9 5.25v-2a.75.75 0 0 0-1.5 0v1.25H1.75a.75.75 0 0 0 0 1.5H4.5v1.25a.75.75 0 0 0 1.5 0v-2Zm9 0a.75.75 0 0 1-.75.75h-6a.75.75 0 0 1 0-1.5h6a.75.75 0 0 1 .75.75Z"></path> </svg> -</button><tool-tip id="tooltip-734a0566-11be-473a-8066-64f19cd60d1b" for="icon-button-feca8c33-1451-4ed5-9a42-b6e9dd3ae8c3" popover="manual" data-direction="s" data-type="label" data-view-component="true" class="sr-only position-absolute">Appearance settings</tool-tip> +</button><tool-tip id="tooltip-7539da9d-7227-4681-99e3-44b606ddc1e6" for="icon-button-4590ff08-b106-405f-a010-79edd4a87cba" popover="manual" data-direction="s" data-type="label" data-view-component="true" class="sr-only position-absolute">Appearance settings</tool-tip> <template data-target="react-partial-anchor.template"> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-react-css.a7c2947c416ec834.module.css" /> @@ -363,7 +363,7 @@ -<qbsearch-input class="search-input" data-scope="owner:buetow" data-custom-scopes-path="/search/custom_scopes" data-delete-custom-scopes-csrf="DW_KUdU-G38_pDOlglrhEwWVnYxlbpev6SOGyGO2kz1nj3W79Eoxp9xdVkoeTEFQKpm6txIMMnZIY_ThwymyTQ" data-max-custom-scopes="10" data-header-redesign-enabled="false" data-initial-value="" data-blackbird-suggestions-path="/search/suggestions" data-jump-to-suggestions-path="/_graphql/GetSuggestedNavigationDestinations" data-current-repository="" data-current-org="" data-current-owner="" data-logged-in="false" data-copilot-chat-enabled="false" data-nl-search-enabled="false" data-retain-scroll-position="true"> +<qbsearch-input class="search-input" data-scope="owner:buetow" data-custom-scopes-path="/search/custom_scopes" data-delete-custom-scopes-csrf="95ZIXXfcM61ahsTAT_za182ownSG8oSj09Xq6jQd_U2Cg4_FcoWK7xbV_UhNQM2jKQkHltrv4gBJ8T5sGuSrnQ" data-max-custom-scopes="10" data-header-redesign-enabled="false" data-initial-value="" data-blackbird-suggestions-path="/search/suggestions" data-jump-to-suggestions-path="/_graphql/GetSuggestedNavigationDestinations" data-current-repository="" data-current-org="" data-current-owner="" data-logged-in="false" data-copilot-chat-enabled="false" data-nl-search-enabled="false" data-retain-scroll-position="true"> <div class="search-input-container search-with-dialog position-relative d-flex flex-row flex-items-center tmp-mr-4 rounded" data-action="click:qbsearch-input#searchInputContainerClicked" @@ -427,7 +427,7 @@ ></div> <div class="QueryBuilder-InputWrapper"> <div aria-hidden="true" class="QueryBuilder-Sizer" data-target="query-builder.sizer"></div> - <input id="query-builder-test" name="query-builder-test" value="" autocomplete="off" type="text" role="combobox" spellcheck="false" aria-expanded="false" aria-describedby="validation-812e8581-9259-406c-8735-9d42ede69d8e" data-target="query-builder.input" data-action=" + <input id="query-builder-test" name="query-builder-test" value="" autocomplete="off" type="text" role="combobox" spellcheck="false" aria-expanded="false" aria-describedby="validation-77e6a7c6-4654-4643-b050-0c6158053233" data-target="query-builder.input" data-action=" input:query-builder#inputChange blur:query-builder#inputBlur keydown:query-builder#inputKeydown @@ -668,7 +668,7 @@ ></ul> </div> - <div class="FormControl-inlineValidation" id="validation-812e8581-9259-406c-8735-9d42ede69d8e" hidden="hidden"> + <div class="FormControl-inlineValidation" id="validation-77e6a7c6-4654-4643-b050-0c6158053233" hidden="hidden"> <span class="FormControl-inlineValidation--visual"> <svg aria-hidden="true" height="12" viewBox="0 0 12 12" version="1.1" width="12" data-view-component="true" class="octicon octicon-alert-fill"> <path d="M4.855.708c.5-.896 1.79-.896 2.29 0l4.675 8.351a1.312 1.312 0 0 1-1.146 1.954H1.33A1.313 1.313 0 0 1 .183 9.058ZM7 7V3H5v4Zm-1 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path> @@ -709,7 +709,7 @@ </div> <scrollable-region data-labelled-by="feedback-dialog-title"> - <div data-view-component="true" class="Overlay-body"> <!-- '"` --><!-- </textarea></xmp> --></option></form><form id="code-search-feedback-form" data-turbo="false" action="/search/feedback" accept-charset="UTF-8" method="post"><input type="hidden" data-csrf="true" name="authenticity_token" value="LDslw0oJEeAQpf+ga6I+y0YtqZGtcmJAOr/z98UOg5SiBn9CHaVuUYnLVxQBSaOXpgnqGWYCgQYllXJHmwdAPA==" /> + <div data-view-component="true" class="Overlay-body"> <!-- '"` --><!-- </textarea></xmp> --></option></form><form id="code-search-feedback-form" data-turbo="false" action="/search/feedback" accept-charset="UTF-8" method="post"><input type="hidden" data-csrf="true" name="authenticity_token" value="PYNfcwKZJRNr9Cv/SpXK3Ar5r3Ov8Q/FYPQJEJ9Km9d8EAt0SZLJF3eoiP/j5Mddho0XTxfLMGUg2Sl5bzMgSA==" /> <p>We read every piece of feedback, and take your input very seriously.</p> <textarea name="feedback" class="form-control width-full mb-2" style="height: 120px" id="feedback"></textarea> <input name="include_email" id="include_email" aria-label="Include my email address so I can be contacted" class="form-control mr-2" type="checkbox"> @@ -747,7 +747,7 @@ <div data-view-component="true" class="Overlay-body"> <div data-target="custom-scopes.customScopesModalDialogFlash"></div> <div hidden class="create-custom-scope-form" data-target="custom-scopes.createCustomScopeForm"> - <!-- '"` --><!-- </textarea></xmp> --></option></form><form id="custom-scopes-dialog-form" data-turbo="false" action="/search/custom_scopes" accept-charset="UTF-8" method="post"><input type="hidden" data-csrf="true" name="authenticity_token" value="+hwgY+qzqjKY4lOdljq03j6Z9A3f9j+sLQGgcY4UbNw8bQmk4tlBb2Y6XbETu/gTeOVOJhFuROV4Sg4LRGxZsw==" /> + <!-- '"` --><!-- </textarea></xmp> --></option></form><form id="custom-scopes-dialog-form" data-turbo="false" action="/search/custom_scopes" accept-charset="UTF-8" method="post"><input type="hidden" data-csrf="true" name="authenticity_token" value="BS587VPUgYJIBSwLj42/5qipKhqrG5gw5Pz/FHrErogsA1/4OYVzkRJj/uDwe2F0hWScojXaxbjO7yMd5US9RQ==" /> <div data-target="custom-scopes.customScopesModalDialogFlash"></div> <input type="hidden" id="custom_scope_id" name="custom_scope_id" data-target="custom-scopes.customScopesIdField"> @@ -765,7 +765,7 @@ placeholder="github-ruby" required maxlength="50"> - <input type="hidden" data-csrf="true" value="Ohyw21LJPLsphkkO/DTqz1q259XtSwwctYPk+PsweCWWYb+dvL9TSuW9KC+lBvchCEFVmCuxNRKyzeNB/fmLLw==" /> + <input type="hidden" data-csrf="true" value="Yn6T0IJHsLRSSTwvmpOPDK+jUeHxYjWHpvANooOG7wnRY7wwHQrAp6rZg7W3jTYHeBWIrmGtZIDDglX+Zfvwaw==" /> </auto-check> </div> @@ -820,7 +820,7 @@ <h4 data-view-component="true" class="color-fg-default mb-2"> Sign in to GitHub </h4> -<!-- '"` --><!-- </textarea></xmp> --></option></form><form data-turbo="false" action="/session" accept-charset="UTF-8" method="post"><input type="hidden" data-csrf="true" name="authenticity_token" value="OmxKg3WlS2rQfywX4neYeXRFzy6Wz3kCQfQhOD70ZHZL2GAs2GM0QMIAfUOAzr/EjJfVlcWbsL/vt4AbbWFr6Q==" /> <input type="hidden" name="add_account" id="add_account" autocomplete="off" class="form-control" /> +<!-- '"` --><!-- </textarea></xmp> --></option></form><form data-turbo="false" action="/session" accept-charset="UTF-8" method="post"><input type="hidden" data-csrf="true" name="authenticity_token" value="rKHzeP+orkON6Q6sggV6HUQSpsO9AGOsWTGMR7VptOs6DW0qIkBy14+1+anIkGPtF3H8g9AxQdIGxN1MBX8/9Q==" /> <input type="hidden" name="add_account" id="add_account" autocomplete="off" class="form-control" /> <label for="login_field"> Username or email address @@ -842,9 +842,9 @@ <input type="hidden" name="allow_signup" id="allow_signup" autocomplete="off" class="form-control" /> <input type="hidden" name="client_id" id="client_id" autocomplete="off" class="form-control" /> <input type="hidden" name="integration" id="integration" autocomplete="off" class="form-control" /> -<input class="form-control" type="text" name="required_field_a481" hidden="hidden" /> -<input class="form-control" type="hidden" name="timestamp" value="1773261629623" /> -<input class="form-control" type="hidden" name="timestamp_secret" value="40ae8de5db80da111368d3b823de3104c83308d4f018d10aec0bc7a2f54aa29c" /> +<input class="form-control" type="text" name="required_field_a5d9" hidden="hidden" /> +<input class="form-control" type="hidden" name="timestamp" value="1773338430530" /> +<input class="form-control" type="hidden" name="timestamp_secret" value="bba05d3419f37fe54d4d68c6d0d94e99459854d78cc37fbd966fce06119a8722" /> <input type="submit" name="commit" value="Sign in" class="btn btn-primary btn-block js-sign-in-button" data-disable-with="Signing in…" data-signin-label="Sign in" data-sso-label="Sign in with your identity provider" development="false" disable-emu-sso="false" /> @@ -871,10 +871,10 @@ <div class="AppHeader-appearanceSettings"> <react-partial-anchor> - <button data-target="react-partial-anchor.anchor" id="icon-button-a3607ef0-75be-4d3f-814c-59c1a9d314fb" aria-labelledby="tooltip-075f6d76-f532-490e-b09e-0d676a99eaf6" type="button" disabled="disabled" data-view-component="true" class="Button Button--iconOnly Button--invisible Button--medium AppHeader-button HeaderMenu-link border cursor-wait"> <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-sliders Button-visual"> + <button data-target="react-partial-anchor.anchor" id="icon-button-c2af4e9b-79a3-48ce-9a22-c89c1417aeb3" aria-labelledby="tooltip-832efbfa-b106-40e8-a890-f707db113cd2" type="button" disabled="disabled" data-view-component="true" class="Button Button--iconOnly Button--invisible Button--medium AppHeader-button HeaderMenu-link border cursor-wait"> <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-sliders Button-visual"> <path d="M15 2.75a.75.75 0 0 1-.75.75h-4a.75.75 0 0 1 0-1.5h4a.75.75 0 0 1 .75.75Zm-8.5.75v1.25a.75.75 0 0 0 1.5 0v-4a.75.75 0 0 0-1.5 0V2H1.75a.75.75 0 0 0 0 1.5H6.5Zm1.25 5.25a.75.75 0 0 0 0-1.5h-6a.75.75 0 0 0 0 1.5h6ZM15 8a.75.75 0 0 1-.75.75H11.5V10a.75.75 0 1 1-1.5 0V6a.75.75 0 0 1 1.5 0v1.25h2.75A.75.75 0 0 1 15 8Zm-9 5.25v-2a.75.75 0 0 0-1.5 0v1.25H1.75a.75.75 0 0 0 0 1.5H4.5v1.25a.75.75 0 0 0 1.5 0v-2Zm9 0a.75.75 0 0 1-.75.75h-6a.75.75 0 0 1 0-1.5h6a.75.75 0 0 1 .75.75Z"></path> </svg> -</button><tool-tip id="tooltip-075f6d76-f532-490e-b09e-0d676a99eaf6" for="icon-button-a3607ef0-75be-4d3f-814c-59c1a9d314fb" popover="manual" data-direction="s" data-type="label" data-view-component="true" class="sr-only position-absolute">Appearance settings</tool-tip> +</button><tool-tip id="tooltip-832efbfa-b106-40e8-a890-f707db113cd2" for="icon-button-c2af4e9b-79a3-48ce-9a22-c89c1417aeb3" popover="manual" data-direction="s" data-type="label" data-view-component="true" class="sr-only position-absolute">Appearance settings</tool-tip> <template data-target="react-partial-anchor.template"> <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-react-css.a7c2947c416ec834.module.css" /> @@ -912,10 +912,10 @@ <span class="js-stale-session-flash-signed-out" hidden>You signed out in another tab or window. <a class="Link--inTextBlock" href="">Reload</a> to refresh your session.</span> <span class="js-stale-session-flash-switched" hidden>You switched accounts on another tab or window. <a class="Link--inTextBlock" href="">Reload</a> to refresh your session.</span> - <button id="icon-button-1b708de1-06ec-4e3e-918a-b0b9ffac2fdf" aria-labelledby="tooltip-f9526ce3-dcc1-447e-85f0-3679958e60bc" type="button" data-view-component="true" class="Button Button--iconOnly Button--invisible Button--medium flash-close js-flash-close"> <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-x Button-visual"> + <button id="icon-button-56eb76cc-1691-499c-8413-5d156f6743dd" aria-labelledby="tooltip-94b431db-9d38-4497-b4d3-577dec6ed8b8" type="button" data-view-component="true" class="Button Button--iconOnly Button--invisible Button--medium flash-close js-flash-close"> <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-x Button-visual"> <path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path> </svg> -</button><tool-tip id="tooltip-f9526ce3-dcc1-447e-85f0-3679958e60bc" for="icon-button-1b708de1-06ec-4e3e-918a-b0b9ffac2fdf" popover="manual" data-direction="s" data-type="label" data-view-component="true" class="sr-only position-absolute">Dismiss alert</tool-tip> +</button><tool-tip id="tooltip-94b431db-9d38-4497-b4d3-577dec6ed8b8" for="icon-button-56eb76cc-1691-499c-8413-5d156f6743dd" popover="manual" data-direction="s" data-type="label" data-view-component="true" class="sr-only position-absolute">Dismiss alert</tool-tip> @@ -1015,7 +1015,7 @@ <div class="tmp-mt-5 color-fg-muted text-center"> <a href="https://support.github.com?tags=dotcom-404" class="Link--secondary">Contact Support</a> — <a href="https://githubstatus.com" class="Link--secondary">GitHub Status</a> — - <a href="https://twitter.com/githubstatus" class="Link--secondary">@githubstatus</a> + <a href="https://x.com/githubstatus" class="Link--secondary">@githubstatus</a> </div> </div> diff --git a/gemfeed/DRAFT-f3s-kubernetes-with-freebsd-part-X.html b/gemfeed/DRAFT-f3s-kubernetes-with-freebsd-part-X.html index 5d58d4c5..2ce86ad7 100644 --- a/gemfeed/DRAFT-f3s-kubernetes-with-freebsd-part-X.html +++ b/gemfeed/DRAFT-f3s-kubernetes-with-freebsd-part-X.html @@ -42,49 +42,32 @@ <li>⇢ ⇢ <a href='#accessing-argocd'>Accessing ArgoCD</a></li> <li>⇢ <a href='#argocd-application-structure'>ArgoCD Application Structure</a></li> <li>⇢ <a href='#repository-organization'>Repository Organization</a></li> -<li>⇢ <a href='#migration-strategy-incremental-one-app-at-a-time'>Migration Strategy: Incremental, One App at a Time</a></li> <li>⇢ ⇢ <a href='#migration-phases'>Migration Phases</a></li> <li>⇢ <a href='#example-migration-miniflux'>Example Migration: Miniflux</a></li> <li>⇢ ⇢ <a href='#before-imperative-helm-deployment'>Before: Imperative Helm deployment</a></li> <li>⇢ ⇢ <a href='#after-declarative-gitops-with-argocd'>After: Declarative GitOps with ArgoCD</a></li> <li>⇢ ⇢ <a href='#migration-procedure'>Migration procedure</a></li> -<li>⇢ <a href='#complex-migration-prometheus-with-multi-source'>Complex Migration: Prometheus with Multi-Source</a></li> -<li>⇢ ⇢ <a href='#sync-waves-and-hooks'>Sync Waves and Hooks</a></li> -<li>⇢ <a href='#migration-results'>Migration Results</a></li> -<li>⇢ <a href='#benefits-realized'>Benefits Realized</a></li> -<li>⇢ ⇢ <a href='#1-single-source-of-truth'>1. Single Source of Truth</a></li> -<li>⇢ ⇢ <a href='#2-automatic-synchronization'>2. Automatic Synchronization</a></li> -<li>⇢ ⇢ <a href='#3-drift-detection-and-self-healing'>3. Drift Detection and Self-Healing</a></li> -<li>⇢ ⇢ <a href='#4-easy-rollbacks'>4. Easy Rollbacks</a></li> -<li>⇢ ⇢ <a href='#5-disaster-recovery'>5. Disaster Recovery</a></li> -<li>⇢ ⇢ <a href='#6-documentation-by-default'>6. Documentation by Default</a></li> -<li>⇢ ⇢ <a href='#7-safe-experimentation'>7. Safe Experimentation</a></li> -<li>⇢ <a href='#challenges-and-solutions'>Challenges and Solutions</a></li> -<li>⇢ ⇢ <a href='#challenge-1-helm-release-adoption'>Challenge 1: Helm Release Adoption</a></li> -<li>⇢ ⇢ <a href='#challenge-2-persistent-volumes-not-tracked-by-helm'>Challenge 2: Persistent Volumes Not Tracked by Helm</a></li> -<li>⇢ ⇢ <a href='#challenge-3-secrets-management'>Challenge 3: Secrets Management</a></li> -<li>⇢ ⇢ <a href='#challenge-4-grafana-not-reloading-datasources'>Challenge 4: Grafana Not Reloading Datasources</a></li> -<li>⇢ ⇢ <a href='#challenge-5-prometheus-with-multiple-sources'>Challenge 5: Prometheus With Multiple Sources</a></li> -<li>⇢ ⇢ <a href='#challenge-6-sync-ordering-for-prometheus'>Challenge 6: Sync Ordering for Prometheus</a></li> -<li>⇢ <a href='#justfile-evolution'>Justfile Evolution</a></li> -<li>⇢ <a href='#lessons-learned'>Lessons Learned</a></li> -<li>⇢ <a href='#future-improvements'>Future Improvements</a></li> -<li>⇢ ⇢ <a href='#1-external-secrets-operator'>1. External Secrets Operator</a></li> -<li>⇢ ⇢ <a href='#2-applicationset-for-similar-apps'>2. ApplicationSet for Similar Apps</a></li> -<li>⇢ ⇢ <a href='#3-app-of-apps-pattern'>3. App-of-Apps Pattern</a></li> -<li>⇢ ⇢ <a href='#4-argocd-image-updater'>4. ArgoCD Image Updater</a></li> -<li>⇢ <a href='#summary'>Summary</a></li> +<li><a href='#argocd-detects-change-within-3-minutes-and-syncs-automatically'>ArgoCD detects change within 3 minutes and syncs automatically</a></li> +<li><a href='#argocd-detects-drift-within-3-minutes'>ArgoCD detects drift within 3 minutes</a></li> +<li><a href='#argocd-automatically-rolls-back-to-the-previous-state'>ArgoCD automatically rolls back to the previous state</a></li> +<li><a href='#temporarily-point-argocd-at-the-feature-branch'>Temporarily point ArgoCD at the feature branch</a></li> +<li><a href='#verify-changes-in-argocd-web-ui'>Verify changes in ArgoCD Web UI</a></li> +<li><a href='#if-good-merge-to-master'>If good: merge to master</a></li> +<li><a href='#if-bad-revert-the-patch'>If bad: revert the patch</a></li> +<li><a href='#root-monitoringyaml'>root-monitoring.yaml</a></li> +<li><a href='#root-app-deploys-all-21-applications-automatically'>Root app deploys all 21 applications automatically</a></li> +<li><a href='#or-apply-by-namespace'>Or apply by namespace</a></li> </ul><br /> <h2 style='display: inline' id='introduction'>Introduction</h2><br /> <br /> <span>In the previous posts, I deployed applications to the k3s cluster using Helm charts and Justfiles—running <span class='inlinecode'>just install</span> or <span class='inlinecode'>just upgrade</span> to imperatively push changes to the cluster. While this approach works, it has several drawbacks:</span><br /> <br /> <ul> -<li>**No single source of truth**: The cluster state depends on which commands were run and when</li> -<li>**Manual synchronization**: Every change requires manually running commands</li> -<li>**Drift detection is hard**: No easy way to know if cluster state matches the desired configuration</li> -<li>**Rollback complexity**: Rolling back changes means re-running old Helm commands</li> -<li>**No audit trail**: Hard to track who changed what and when</li> +<li>No single source of truth: The cluster state depends on which commands were run and when</li> +<li>Manual synchronization: Every change requires manually running commands</li> +<li>Drift detection is hard: No easy way to know if cluster state matches the desired configuration</li> +<li>Rollback complexity: Rolling back changes means re-running old Helm commands</li> +<li>No audit trail: Hard to track who changed what and when</li> </ul><br /> <span>This blog post covers the migration from imperative Helm deployments to declarative GitOps using ArgoCD. After this migration, the Git repository becomes the single source of truth, and ArgoCD automatically ensures the cluster matches what's defined in Git.</span><br /> <br /> @@ -95,18 +78,19 @@ <span>Key principles:</span><br /> <br /> <ul> -<li>**Declarative**: The system's desired state is described declaratively (YAML manifests, Helm values)</li> -<li>**Versioned and immutable**: All changes are committed to Git, providing a complete history</li> -<li>**Pulled automatically**: An agent in the cluster continuously pulls the desired state from Git</li> -<li>**Continuously reconciled**: The agent ensures the actual state matches the desired state, automatically correcting drift</li> +<li>Declarative: The system's desired state is described declaratively (YAML manifests, Helm values)</li> +<li>Versioned and immutable: All changes are committed to Git, providing a complete history</li> +<li>Pulled automatically: An agent in the cluster continuously pulls the desired state from Git</li> +<li>Continuously reconciled: The agent ensures the actual state matches the desired state, automatically correcting drift</li> </ul><br /> <span>For Kubernetes, this means:</span><br /> <br /> -<span>1. All manifests, Helm charts, and configuration live in a Git repository</span><br /> -<span>2. A tool (ArgoCD in our case) watches the repository</span><br /> -<span>3. When changes are pushed to Git, ArgoCD automatically applies them to the cluster</span><br /> -<span>4. If someone manually changes resources in the cluster, ArgoCD detects the drift and can automatically revert it</span><br /> -<br /> +<ul> +<li>1. All manifests, Helm charts, and configuration live in a Git repository</li> +<li>2. A tool (ArgoCD in our case) watches the repository</li> +<li>3. When changes are pushed to Git, ArgoCD automatically applies them to the cluster</li> +<li>4. If someone manually changes resources in the cluster, ArgoCD detects the drift and can automatically revert it</li> +</ul><br /> <h2 style='display: inline' id='what-is-argocd'>What is ArgoCD?</h2><br /> <br /> <span>ArgoCD is a declarative, GitOps continuous delivery tool for Kubernetes. It's implemented as a Kubernetes controller that continuously monitors running applications and compares the current, live state against the desired target state defined in Git.</span><br /> @@ -116,32 +100,34 @@ <span>Key features:</span><br /> <br /> <ul> -<li>**Automated deployment**: Monitors Git repositories and automatically syncs changes to the cluster</li> -<li>**Application definitions**: Defines applications as CRDs (Custom Resource Definitions)</li> -<li>**Health assessment**: Understands Kubernetes resources and can determine if an application is healthy</li> -<li>**Web UI and CLI**: Provides both a web interface and command-line tool for managing applications</li> -<li>**RBAC**: Role-based access control for team collaboration</li> -<li>**SSO integration**: Can integrate with existing authentication systems</li> -<li>**Multi-cluster support**: Can manage applications across multiple Kubernetes clusters</li> -<li>**Sync waves and hooks**: Control the order of resource deployment and run jobs at specific lifecycle points</li> +<li>Automated deployment: Monitors Git repositories and automatically syncs changes to the cluster</li> +<li>Application definitions: Defines applications as CRDs (Custom Resource Definitions)</li> +<li>Health assessment: Understands Kubernetes resources and can determine if an application is healthy</li> +<li>Web UI and CLI: Provides both a web interface and command-line tool for managing applications</li> +<li>RBAC: Role-based access control for team collaboration</li> +<li>SSO integration: Can integrate with existing authentication systems</li> +<li>Multi-cluster support: Can manage applications across multiple Kubernetes clusters</li> +<li>Sync waves and hooks: Control the order of resource deployment and run jobs at specific lifecycle points</li> </ul><br /> <h2 style='display: inline' id='why-argocd-for-f3s'>Why ArgoCD for f3s?</h2><br /> <br /> <span>For a home lab cluster, ArgoCD provides several benefits:</span><br /> <br /> -<span>**Disaster recovery**: If the entire cluster is lost, I can rebuild it by:</span><br /> -<span>1. Bootstrapping a new k3s cluster</span><br /> -<span>2. Installing ArgoCD</span><br /> -<span>3. Pointing ArgoCD at the Git repository</span><br /> -<span>4. All applications automatically deploy to the desired state</span><br /> +<span>Disaster recovery: If the entire cluster is lost, I can rebuild it by:</span><br /> <br /> -<span>**Experimentation safety**: I can test changes in a separate Git branch without affecting the running cluster. Once validated, merge to master and ArgoCD applies the changes.</span><br /> -<br /> -<span>**Drift detection**: If I manually change something in the cluster (for debugging), ArgoCD shows the difference and can automatically revert it.</span><br /> +<ul> +<li>1. Bootstrapping a new k3s cluster</li> +<li>2. Installing ArgoCD</li> +<li>3. Pointing ArgoCD at the Git repository</li> +<li>4. All applications automatically deploy to the desired state</li> +</ul><br /> +<span>Experimentation safety: I can test changes in a separate Git branch without affecting the running cluster. Once validated, merge to master and ArgoCD applies the changes.</span><br /> +<span> </span><br /> +<span>Drift detection: If I manually change something in the cluster (for debugging), ArgoCD shows the difference and can automatically revert it.</span><br /> <br /> -<span>**Declarative configuration**: The Git repository documents the entire cluster configuration. No need to remember which <span class='inlinecode'>just</span> commands to run or in which order.</span><br /> +<span>Declarative configuration: The Git repository documents the entire cluster configuration. No need to remember which <span class='inlinecode'>just</span> commands to run or in which order.</span><br /> <br /> -<span>**Automatic sync**: Push to Git, and changes deploy automatically. No need to SSH to a workstation and run Helm commands.</span><br /> +<span>Automatic sync: Push to Git, and changes deploy automatically. No need to SSH to a workstation and run Helm commands.</span><br /> <br /> <h2 style='display: inline' id='deploying-argocd'>Deploying ArgoCD</h2><br /> <br /> @@ -187,7 +173,7 @@ STATUS: deployed <br /> <span>The <span class='inlinecode'>values.yaml</span> file configures several important aspects:</span><br /> <br /> -<span>**Persistent storage for the repo-server**: ArgoCD clones Git repositories to cache them locally. I configured a persistent volume so the cache survives pod restarts:</span><br /> +<span>Persistent storage for the repo-server: ArgoCD clones Git repositories to cache them locally. I configured a persistent volume so the cache survives pod restarts:</span><br /> <br /> <pre> repoServer: @@ -200,7 +186,7 @@ repoServer: mountPath: /tmp </pre> <br /> -<span>**Admin password preservation**: By default, the admin password is auto-generated and stored in a secret. To ensure it persists across Helm upgrades:</span><br /> +<span>Admin password preservation: By default, the admin password is auto-generated and stored in a secret. To ensure it persists across Helm upgrades:</span><br /> <br /> <pre> configs: @@ -222,7 +208,7 @@ $ kubectl create secret generic argocd-secret \ $ echo <font color="#808080">"ArgoCD admin password: $ARGOCD_ADMIN_PASSWORD"</font> </pre> <br /> -<span>**Server configuration**: Enabled insecure mode since TLS is handled by the OpenBSD edge relays:</span><br /> +<span>Server configuration: Enabled insecure mode since TLS is handled by the OpenBSD edge relays:</span><br /> <br /> <pre> server: @@ -261,7 +247,7 @@ metadata: traefik.ingress.kubernetes.io/router.entrypoints: web spec: rules: - - host: argocd.f3s.buetow.org + - host: argocd.f3s.foo.zone http: paths: - path: / @@ -275,15 +261,13 @@ spec: <br /> <span>Following the same pattern as other services, the OpenBSD edge relays terminate TLS and forward traffic through WireGuard to the cluster. ArgoCD is now accessible at:</span><br /> <br /> -<a class='textlink' href='https://argocd.f3s.buetow.org'>ArgoCD Web UI</a><br /> -<br /> <span>The ArgoCD CLI can also be used for operations:</span><br /> <br /> <!-- Generator: GNU source-highlight 3.1.9 by Lorenzo Bettini http://www.lorenzobettini.it http://www.gnu.org/software/src-highlite --> -<pre>$ argocd login argocd.f3s.buetow.org +<pre>$ argocd login argocd.f3s.foo.zone $ argocd app list </pre> <br /> @@ -292,15 +276,13 @@ $ argocd app list <span>ArgoCD uses a CRD called <span class='inlinecode'>Application</span> to define what should be deployed. Each application specifies:</span><br /> <br /> <ul> -<li>**Source**: Where the manifests live (Git repo, Helm chart repository, or both)</li> -<li>**Destination**: Which cluster and namespace to deploy to</li> -<li>**Sync policy**: Whether to automatically sync changes</li> +<li>Source: Where the manifests live (Git repo, Helm chart repository, or both)</li> +<li>Destination: Which cluster and namespace to deploy to</li> </ul><br /> <span>Here's a simple example for the miniflux application:</span><br /> <br /> <pre> -apiVersion: argoproj.io/v1alpha1 -kind: Application +ind: Application metadata: name: miniflux namespace: cicd @@ -310,9 +292,11 @@ spec: project: default source: repoURL: https://codeberg.org/snonux/conf.git + targetRevision: master path: f3s/miniflux/helm-chart destination: + server: https://kubernetes.default.svc namespace: services syncPolicy: @@ -389,57 +373,44 @@ spec: <br /> <span>The application directories (miniflux, prometheus, etc.) remained mostly unchanged—ArgoCD references the same Helm charts. The main additions:</span><br /> <br /> -<span>1. **argocd-apps/**: Application manifests organized by Kubernetes namespace for better clarity</span><br /> -<span> - <span class='inlinecode'>monitoring/</span>: 6 observability applications</span><br /> -<span> - <span class='inlinecode'>services/</span>: 13 user-facing applications</span><br /> -<span> - <span class='inlinecode'>infra/</span>: 1 infrastructure application (registry)</span><br /> -<span> - <span class='inlinecode'>test/</span>: 1 test application</span><br /> -<span>2. ***/manifests/**: Additional Kubernetes manifests for complex apps (like Prometheus)</span><br /> -<span>3. **Justfiles updated**: Changed from <span class='inlinecode'>helm install/upgrade</span> to <span class='inlinecode'>argocd app sync</span></span><br /> -<br /> -<span>This organization makes it easy to apply all applications in a specific namespace or manage them independently.</span><br /> -<br /> -<h2 style='display: inline' id='migration-strategy-incremental-one-app-at-a-time'>Migration Strategy: Incremental, One App at a Time</h2><br /> +<span>1. argocd-apps/: Application manifests organized by Kubernetes namespace for better clarity</span><br /> <br /> -<span>Rather than attempting a "big bang" migration of all 21 applications at once, I migrated them incrementally:</span><br /> -<br /> -<span>1. **Start with a simple app**: Validate the pattern with a low-risk application</span><br /> -<span>2. **Migrate in waves**: Group similar applications and migrate together</span><br /> -<span>3. **Validate thoroughly**: Ensure each app is healthy before moving to the next</span><br /> -<span>4. **Learn and iterate**: Apply lessons from earlier migrations to later ones</span><br /> +<ul> +<li><span class='inlinecode'>monitoring/</span>: 6 observability applications</li> +<li><span class='inlinecode'>services/</span>: 13 user-facing applications</li> +<li><span class='inlinecode'>infra/</span>: 1 infrastructure application (registry)</li> +<li><span class='inlinecode'>test/</span>: 1 test application</li> +</ul><br /> +<span>2. */manifests/: Additional Kubernetes manifests for complex apps (like Prometheus)</span><br /> +<span>3. Justfiles updated: Changed from <span class='inlinecode'>helm install/upgrade</span> to <span class='inlinecode'>argocd app sync</span></span><br /> <br /> -<span>This approach reduced risk and allowed me to refine the migration process.</span><br /> +<span>This organization makes it easy to apply all applications in a specific namespace or manage them independently.</span><br /> <br /> <h3 style='display: inline' id='migration-phases'>Migration Phases</h3><br /> <br /> -<span>**Phase 1: Simple services** (13 apps)</span><br /> +<span>These apps have straightforward Helm charts with no complex dependencies. Pattern established:</span><br /> +<br /> <ul> -<li>miniflux, freshrss, wallabag</li> -<li>anki-sync-server, kobo-sync-server, opodsync</li> -<li>radicale, syncthing, audiobookshelf</li> -<li>filebrowser, keybr, webdav</li> -<li>example-apache, example-apache-volume-claim</li> +<li>1. Create Application manifest in <span class='inlinecode'>argocd-apps/</span></li> +<li>2. Apply with <span class='inlinecode'>kubectl apply -f argocd-apps/<app>.yaml</span></li> +<li>3. Verify sync status: <span class='inlinecode'>argocd app get <app></span></li> +<li>4. Update Justfile to use ArgoCD commands</li> </ul><br /> -<span>These apps have straightforward Helm charts with no complex dependencies. Pattern established:</span><br /> -<span>1. Create Application manifest in <span class='inlinecode'>argocd-apps/</span></span><br /> -<span>2. Apply with <span class='inlinecode'>kubectl apply -f argocd-apps/<app>.yaml</span></span><br /> -<span>3. Verify sync status: <span class='inlinecode'>argocd app get <app></span></span><br /> -<span>4. Update Justfile to use ArgoCD commands</span><br /> +<span>Phase 2: Infrastructure apps (3 apps)</span><br /> <br /> -<span>**Phase 2: Infrastructure apps** (3 apps)</span><br /> <ul> <li>registry (Docker image registry)</li> <li>pushgateway (Prometheus metrics ingestion)</li> <li>immich (photo management with complex dependencies)</li> </ul><br /> -<span>**Phase 3: Monitoring stack** (4 apps)</span><br /> +<span>Phase 3: Monitoring stack (4 apps)</span><br /> <ul> <li>tempo (distributed tracing)</li> <li>loki (log aggregation)</li> <li>alloy (log collection)</li> <li>prometheus (metrics and monitoring)</li> </ul><br /> -<span>**Phase 4: Monitoring addons** (1 app)</span><br /> +<span>Phase 4: Monitoring addons (1 app)</span><br /> <ul> <li>grafana-ingress (separate ingress for Grafana)</li> </ul><br /> @@ -553,7 +524,7 @@ logs: <br /> <h3 style='display: inline' id='migration-procedure'>Migration procedure</h3><br /> <br /> -<span>1. **Backup current state**:</span><br /> +<span>1. Backup current state:</span><br /> <!-- Generator: GNU source-highlight 3.1.9 by Lorenzo Bettini http://www.lorenzobettini.it @@ -562,7 +533,7 @@ http://www.gnu.org/software/src-highlite --> $ kubectl get all,ingress -n services -o yaml > /tmp/miniflux-backup.yaml </pre> <br /> -<span>2. **Create Application manifest**:</span><br /> +<span>2. Create Application manifest:</span><br /> <!-- Generator: GNU source-highlight 3.1.9 by Lorenzo Bettini http://www.lorenzobettini.it @@ -571,7 +542,7 @@ http://www.gnu.org/software/src-highlite --> application.argoproj.io/miniflux created </pre> <br /> -<span>3. **Verify ArgoCD adopted the resources**:</span><br /> +<span>3. Verify ArgoCD adopted the resources:</span><br /> <!-- Generator: GNU source-highlight 3.1.9 by Lorenzo Bettini http://www.lorenzobettini.it @@ -581,7 +552,7 @@ Name: miniflux Project: default Server: https://kubernetes.default.svc Namespace: services -URL: https://argocd.f3s.buetow.org/applications/miniflux +URL: https://argocd.f3s.foo.zone/applications/miniflux Repo: https://codeberg.org/snonux/conf.git Target: master Path: f3s/miniflux/helm-chart @@ -591,7 +562,7 @@ Sync Status: Synced to master (4e3c216) Health Status: Healthy </pre> <br /> -<span>4. **Monitor for issues**:</span><br /> +<span>4. Monitor for issues:</span><br /> <!-- Generator: GNU source-highlight 3.1.9 by Lorenzo Bettini http://www.lorenzobettini.it @@ -599,623 +570,500 @@ http://www.gnu.org/software/src-highlite --> <pre>$ kubectl get pods -n services -l app=miniflux -w NAME READY STATUS RESTARTS AGE miniflux-postgres-556444cb8d-xvv2p <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">0</font> 54d -miniflux-server-85d7c64664-stmt<font color="#000000">9</font> <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">0</font> 54d -</pre> -<br /> -<span>5. **Test the application**:</span><br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ curl -I https://flux.f3s.buetow.org -HTTP/<font color="#000000">2</font> <font color="#000000">200</font> +`` + +<font color="#000000">5</font>. Test the application: </pre> -<br /> -<span>6. **Update Justfile** and commit changes</span><br /> -<br /> -<span>Total time: 10 minutes. Zero downtime.</span><br /> -<br /> -<h2 style='display: inline' id='complex-migration-prometheus-with-multi-source'>Complex Migration: Prometheus with Multi-Source</h2><br /> -<br /> -<span>The Prometheus migration was more complex because it combines:</span><br /> -<ul> -<li>Upstream Helm chart (kube-prometheus-stack)</li> -<li>Custom manifests (PersistentVolumes, recording rules, dashboards)</li> -<li>Sync hooks (PostSync job to restart Grafana)</li> -</ul><br /> -<span>ArgoCD supports "multi-source" Applications that combine multiple sources:</span><br /> -<br /> +<span>$ curl -I https://flux.f3s.foo.zone</span><br /> +<span>HTTP/2 200</span><br /> <pre> -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: prometheus - namespace: cicd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - sources: - # Source 1: Upstream Helm chart from prometheus-community - - repoURL: https://prometheus-community.github.io/helm-charts - chart: kube-prometheus-stack - targetRevision: 55.5.0 - helm: - releaseName: prometheus - valuesObject: - # Full Prometheus configuration embedded here - kubeEtcd: - enabled: true - endpoints: - - 192.168.2.120 - - 192.168.2.121 - - 192.168.2.122 - # ... (hundreds of lines of configuration) - - # Source 2: Additional manifests from Git repository - - repoURL: https://codeberg.org/snonux/conf.git - targetRevision: master - path: f3s/prometheus/manifests +6. Update Justfile and commit changes - destination: - server: https://kubernetes.default.svc - namespace: monitoring +Total time: 10 minutes. Zero downtime. + +## Complex Migration: Prometheus with Multi-Source + + +The Prometheus migration was more complex because it combines: +* Upstream Helm chart (kube-prometheus-stack) +* Custom manifests (PersistentVolumes, recording rules, dashboards) +* Sync hooks (PostSync job to restart Grafana) - syncPolicy: - automated: - prune: false # Manual pruning for safety on complex stack - selfHeal: true - syncOptions: - - CreateNamespace=false - - ServerSideApply=true - retry: - limit: 3 - backoff: - duration: 10s - factor: 2 - maxDuration: 3m </pre> -<br /> -<span>The <span class='inlinecode'>prometheus/manifests/</span> directory contains:</span><br /> -<br /> +<span>apiVersion: argoproj.io/v1alpha1</span><br /> +<span>kind: Application</span><br /> +<span>metadata:</span><br /> +<span> name: prometheus</span><br /> +<span> namespace: cicd</span><br /> +<span> finalizers:</span><br /> +<span> - resources-finalizer.argocd.argoproj.io</span><br /> +<span>spec:</span><br /> +<span> project: default</span><br /> +<span> sources:</span><br /> +<span> # Source 1: Upstream Helm chart from prometheus-community</span><br /> +<span> - repoURL: https://prometheus-community.github.io/helm-charts</span><br /> +<span> </span><br /> +<span> chart: kube-prometheus-stack</span><br /> +<span> targetRevision: 55.5.0</span><br /> +<span> helm:</span><br /> +<span> releaseName: prometheus</span><br /> +<span> valuesObject:</span><br /> +<span> # Full Prometheus configuration embedded here</span><br /> +<span> kubeEtcd:</span><br /> +<span> enabled: true</span><br /> +<span> endpoints:</span><br /> +<span> - 192.168.2.120</span><br /> +<span> - 192.168.2.121</span><br /> +<span> - 192.168.2.122</span><br /> +<span> # ... (hundreds of lines of configuration)</span><br /> +<br /> +<span> # Source 2: Additional manifests from Git repository</span><br /> +<span> - repoURL: https://codeberg.org/snonux/conf.git</span><br /> +<span> targetRevision: master</span><br /> +<span> path: f3s/prometheus/manifests</span><br /> +<br /> +<span> destination:</span><br /> +<span> server: https://kubernetes.default.svc</span><br /> +<span> namespace: monitoring</span><br /> +<br /> +<span> syncPolicy:</span><br /> +<span> automated:</span><br /> +<span> prune: false # Manual pruning for safety on complex stack</span><br /> +<span> selfHeal: true</span><br /> +<span> syncOptions:</span><br /> +<span> - CreateNamespace=false</span><br /> +<span> - ServerSideApply=true</span><br /> +<span> retry:</span><br /> +<span> limit: 3</span><br /> +<span> backoff:</span><br /> +<span> duration: 10s</span><br /> +<span> factor: 2</span><br /> +<span> maxDuration: 3m</span><br /> <pre> -f3s/prometheus/manifests/ -├── persistent-volumes.yaml # Sync wave 0 -├── additional-scrape-configs-secret.yaml # Sync wave 1 -├── grafana-datasources-configmap.yaml # Sync wave 1 -├── freebsd-recording-rules.yaml # Sync wave 3 -├── openbsd-recording-rules.yaml # Sync wave 3 -├── zfs-recording-rules.yaml # Sync wave 3 -├── epimetheus-dashboard.yaml # Sync wave 4 -├── zfs-dashboards.yaml # Sync wave 4 -├── grafana-restart-hook.yaml # Sync wave 10 (PostSync) -└── grafana-restart-rbac.yaml # Sync wave 0 +The `prometheus/manifests/` directory contains: + </pre> -<br /> -<h3 style='display: inline' id='sync-waves-and-hooks'>Sync Waves and Hooks</h3><br /> -<br /> -<span>ArgoCD allows controlling the order of resource deployment using sync waves (the <span class='inlinecode'>argocd.argoproj.io/sync-wave</span> annotation):</span><br /> -<br /> -<ul> -<li>**Wave 0**: Infrastructure (PersistentVolumes, RBAC)</li> -<li>**Wave 1**: Configuration (Secrets, ConfigMaps)</li> -<li>**Wave 3**: Recording rules (PrometheusRule CRDs)</li> -<li>**Wave 4**: Dashboards (ConfigMaps with <span class='inlinecode'>grafana_dashboard: '1'</span> label)</li> -<li>**Wave 10**: PostSync hooks (Jobs that run after everything else)</li> -</ul><br /> -<span>The Grafana restart hook ensures Grafana reloads datasources after they're updated:</span><br /> -<br /> +<span>f3s/prometheus/manifests/</span><br /> +<span>├── persistent-volumes.yaml # Sync wave 0</span><br /> +<span>├── additional-scrape-configs-secret.yaml # Sync wave 1</span><br /> +<span>├── grafana-datasources-configmap.yaml # Sync wave 1</span><br /> +<span>├── freebsd-recording-rules.yaml # Sync wave 3</span><br /> +<span>├── openbsd-recording-rules.yaml # Sync wave 3</span><br /> +<span>├── zfs-recording-rules.yaml # Sync wave 3</span><br /> +<span>├── epimetheus-dashboard.yaml # Sync wave 4</span><br /> +<span>├── zfs-dashboards.yaml # Sync wave 4</span><br /> +<span>├── grafana-restart-hook.yaml # Sync wave 10 (PostSync)</span><br /> +<span>└── grafana-restart-rbac.yaml # Sync wave 0</span><br /> <pre> -apiVersion: batch/v1 -kind: Job -metadata: - name: grafana-restart-hook - namespace: monitoring - annotations: - argocd.argoproj.io/hook: PostSync - argocd.argoproj.io/hook-delete-policy: BeforeHookCreation - argocd.argoproj.io/sync-wave: "10" -spec: - template: - spec: - serviceAccountName: grafana-restart-sa - restartPolicy: OnFailure - containers: - - name: kubectl - image: bitnami/kubectl:latest - command: - - /bin/sh - - -c - - | - kubectl wait --for=condition=available --timeout=300s deployment/prometheus-grafana -n monitoring || true - kubectl delete pod -n monitoring -l app.kubernetes.io/name=grafana --ignore-not-found=true - backoffLimit: 2 -</pre> -<br /> -<span>This replaces the manual step in the old Justfile that required running <span class='inlinecode'>kubectl delete pod</span> after every upgrade.</span><br /> -<br /> -<h2 style='display: inline' id='migration-results'>Migration Results</h2><br /> -<br /> -<span>After migrating all 21 applications to ArgoCD:</span><br /> -<br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ argocd app list -NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY -alloy https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune -anki-sync-server https://kubernetes.default.svc services default Synced Healthy Auto-Prune -audiobookshelf https://kubernetes.default.svc services default Synced Healthy Auto-Prune -example-apache https://kubernetes.default.svc <b><u><font color="#000000">test</font></u></b> default Synced Healthy Auto-Prune -example-apache-volume-... https://kubernetes.default.svc <b><u><font color="#000000">test</font></u></b> default Synced Healthy Auto-Prune -filebrowser https://kubernetes.default.svc services default Synced Healthy Auto-Prune -freshrss https://kubernetes.default.svc services default Synced Healthy Auto-Prune -grafana-ingress https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune -immich https://kubernetes.default.svc services default Synced Healthy Auto-Prune -keybr https://kubernetes.default.svc services default Synced Healthy Auto-Prune -kobo-sync-server https://kubernetes.default.svc services default Synced Healthy Auto-Prune -loki https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune -miniflux https://kubernetes.default.svc services default Synced Healthy Auto-Prune -opodsync https://kubernetes.default.svc services default Synced Healthy Auto-Prune -prometheus https://kubernetes.default.svc monitoring default Synced Healthy Auto -pushgateway https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune -radicale https://kubernetes.default.svc services default Synced Healthy Auto-Prune -registry https://kubernetes.default.svc infra default Synced Healthy Auto-Prune -syncthing https://kubernetes.default.svc services default Synced Healthy Auto-Prune -tempo https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune -wallabag https://kubernetes.default.svc services default Synced Healthy Auto-Prune -webdav https://kubernetes.default.svc services default Synced Healthy Auto-Prune +### Sync Waves and Hooks + +ArgoCD allows controlling the order of resource deployment using sync waves (the `argocd.argoproj.io/sync-wave` annotation): + +* Wave 0: Infrastructure (PersistentVolumes, RBAC) +* Wave 1: Configuration (Secrets, ConfigMaps) +* Wave 3: Recording rules (PrometheusRule CRDs) +* Wave 4: Dashboards (ConfigMaps with `grafana_dashboard: '1'` label) +* Wave 10: PostSync hooks (Jobs that run after everything else) + +The Grafana restart hook ensures Grafana reloads datasources after they're updated: + </pre> -<br /> -<span>All 21 applications: **Synced** and **Healthy**.</span><br /> -<br /> -<span>ArgoCD Web UI:</span><br /> -<br /> -<a href='./f3s-kubernetes-with-freebsd-part-X/argocd-apps-list.png'><img alt='ArgoCD Applications List' title='ArgoCD Applications List' src='./f3s-kubernetes-with-freebsd-part-X/argocd-apps-list.png' /></a><br /> -<br /> -<a href='./f3s-kubernetes-with-freebsd-part-X/argocd-app-tree.png'><img alt='ArgoCD Application Resource Tree' title='ArgoCD Application Resource Tree' src='./f3s-kubernetes-with-freebsd-part-X/argocd-app-tree.png' /></a><br /> -<br /> -<h2 style='display: inline' id='benefits-realized'>Benefits Realized</h2><br /> -<br /> -<h3 style='display: inline' id='1-single-source-of-truth'>1. Single Source of Truth</h3><br /> -<br /> -<span>The Git repository at <span class='inlinecode'>https://codeberg.org/snonux/conf</span> now contains the complete cluster configuration. Anyone can clone it and see exactly what's deployed:</span><br /> -<br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ git clone https://codeberg.org/snonux/conf.git -$ cd conf/f3s -$ ls argocd-apps/ -alloy.yaml anki-sync-server.yaml audiobookshelf.yaml ... +<span>apiVersion: batch/v1</span><br /> +<span>kind: Job</span><br /> +<span>metadata:</span><br /> +<span> name: grafana-restart-hook</span><br /> +<span> namespace: monitoring</span><br /> +<span> annotations:</span><br /> +<span> argocd.argoproj.io/hook: PostSync</span><br /> +<span>*rgocd.argoproj.io/hook-delete-policy: BeforeHookCreation</span><br /> +<span>*rgocd.argoproj.io/sync-wave: "10"</span><br /> +<span>*</span><br /> +<span> *plate:</span><br /> +<span> spec:</span><br /> +<span> serviceAccountName: grafana-restart-sa</span><br /> +<span> restartPolicy: OnFailure</span><br /> +<span> containers:</span><br /> +<span> - name: kubectl</span><br /> +<span> image: bitnami/kubectl:latest</span><br /> +<span> command:</span><br /> +<span> - /bin/sh</span><br /> +<span> - -c</span><br /> +<span> - |</span><br /> +<span> kubectl wait --for=condition=available --timeout=300s deployment/prometheus-grafana -n monitoring || true</span><br /> +<span> kubectl delete pod -n monitoring -l app.kubernetes.io/name=grafana --ignore-not-found=true</span><br /> +<span> backoffLimit: 2</span><br /> +<pre> +This *he manual step in the old Justfile that required running `kubectl delete pod` after every upgrade. + +## Migration *sults + +After * all 21 applications to ArgoCD: + </pre> -<br /> -<h3 style='display: inline' id='2-automatic-synchronization'>2. Automatic Synchronization</h3><br /> -<br /> -<span>Push to Git, and changes deploy automatically:</span><br /> -<br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ cd conf/f3s/miniflux/helm-chart -$ vim values.yaml <i><font color="silver"># Change replica count from 1 to 2</font></i> -$ git add values.yaml -$ git commit -m <font color="#808080">"Scale miniflux to 2 replicas"</font> -$ git push -<i><font color="silver"># ArgoCD detects change within 3 minutes and syncs automatically</font></i> +<span>$ argocd app *st</span><br /> +<span>NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY</span><br /> +<span>alloy https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune</span><br /> +<span>anki-sync-server https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>audiobookshelf https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>example-apache https://kubernetes.default.svc test default Synced Healthy Auto-Prune</span><br /> +<span>example-apache-volume-... https://kubernetes.default.svc test default Synced Healthy Auto-Prune</span><br /> +<span>filebrowser https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>freshrss https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>grafana-ingress https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune</span><br /> +<span>immich https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>keybr https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>kobo-sync-server https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>loki https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune</span><br /> +<span>miniflux https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>opodsync https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>prometheus https://kubernetes.default.svc monitoring default Synced Healthy Auto</span><br /> +<span>pushgateway https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune</span><br /> +<span>radicale https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>registry https://kubernetes.default.svc infra default Synced Healthy Auto-Prune</span><br /> +<span>syncthing https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>tempo https://kubernetes.default.svc monitoring default Synced Healthy Auto-Prune</span><br /> +<span>wallabag https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<span>webdav https://kubernetes.default.svc services default Synced Healthy Auto-Prune</span><br /> +<pre> +All 21 applications: Synced and Healthy. + +ArgoCD Web UI: + +=> ./f3s-kubernetes-with-freebsd-part-X/argocd-apps-list.png ArgoCD Applications List + +=> ./f3s-kubernetes-with-freebsd-part-X/argocd-app-tree.png ArgoCD Application Resource Tree + +## Benefits Realized + +### 1. Single Source of Truth + +The Git repository at `https://codeberg.org/snonux/conf` now contains the complete cluster configuration. Anyone can clone it and see exactly what's deployed: + </pre> -<br /> -<span>No need to SSH to a workstation, pull the repo, and run <span class='inlinecode'>just upgrade</span>.</span><br /> -<br /> -<h3 style='display: inline' id='3-drift-detection-and-self-healing'>3. Drift Detection and Self-Healing</h3><br /> -<br /> -<span>If someone manually changes a resource in the cluster, ArgoCD detects it:</span><br /> -<br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ kubectl scale deployment miniflux-server -n services --replicas=<font color="#000000">3</font> -deployment.apps/miniflux-server scaled +<span>$ git clone https://codeberg.org/snonux/conf.git</span><br /> +<span>$ cd conf/f3s</span><br /> +<span>$ ls argocd-apps/</span><br /> +<span>alloy.yaml anki-sync-server.yaml audiobookshelf.yaml ...</span><br /> +<pre> +### 2. Automatic Synchronization + +Push to Git, and changes deploy automatically: -<i><font color="silver"># ArgoCD detects drift within 3 minutes</font></i> -$ argocd app get miniflux -... -Sync Status: OutOfSync from master (4e3c216) </pre> -<br /> -<span>With <span class='inlinecode'>selfHeal: true</span>, ArgoCD automatically reverts the change back to 2 replicas (the value in Git).</span><br /> -<br /> -<h3 style='display: inline' id='4-easy-rollbacks'>4. Easy Rollbacks</h3><br /> -<br /> -<span>To rollback a change:</span><br /> -<br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ git revert HEAD -$ git push -<i><font color="silver"># ArgoCD automatically rolls back to the previous state</font></i> +<span>$ cd conf/f3s/miniflux/helm-chart</span><br /> +<span>$ vim values.yaml # Change replica count from 1 to 2</span><br /> +<span>$ git add values.yaml</span><br /> +<span>$ git commit -m "Scale miniflux to 2 replicas"</span><br /> +<span>$ git push</span><br /> +<h1 style='display: inline' id='argocd-detects-change-within-3-minutes-and-syncs-automatically'>ArgoCD detects change within 3 minutes and syncs automatically</h1><br /> +<pre> +No need to SSH to a workstation, pull the repo, and run `just upgrade`. + +### 3. Drift Detection and Self-Healing + +If someone manually changes a resource in the cluster, ArgoCD detects it: + </pre> +<span>$ kubectl scale deployment miniflux-server -n services --replicas=3</span><br /> +<span>deployment.apps/miniflux-server scaled</span><br /> <br /> -<span>Or rollback to a specific commit:</span><br /> -<br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ argocd app rollback miniflux <revision-id> +<h1 style='display: inline' id='argocd-detects-drift-within-3-minutes'>ArgoCD detects drift within 3 minutes</h1><br /> +<span>$ argocd app get miniflux</span><br /> +<span>...</span><br /> +<span>Sync Status: OutOfSync from master (4e3c216)</span><br /> +<pre> +With `selfHeal: true`, ArgoCD automatically reverts the change back to 2 replicas (the value in Git). + +### 4. Easy Rollbacks + +To rollback a change: + </pre> -<br /> -<h3 style='display: inline' id='5-disaster-recovery'>5. Disaster Recovery</h3><br /> -<br /> -<span>If the entire cluster is destroyed, recovery is straightforward:</span><br /> -<br /> -<span>1. Bootstrap a new k3s cluster</span><br /> -<span>2. Create namespaces</span><br /> -<span>3. Install ArgoCD</span><br /> -<span>4. Apply all Application manifests:</span><br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ kubectl apply -f argocd-apps/ +<span>$ git revert HEAD</span><br /> +<span>$ git push</span><br /> +<h1 style='display: inline' id='argocd-automatically-rolls-back-to-the-previous-state'>ArgoCD automatically rolls back to the previous state</h1><br /> +<pre> +Or rollback to a specific commit: + </pre> -<span>5. ArgoCD deploys all 21 applications to their desired state</span><br /> -<br /> -<span>Total recovery time: ~30 minutes (mostly waiting for pods to pull images and start).</span><br /> -<br /> -<h3 style='display: inline' id='6-documentation-by-default'>6. Documentation by Default</h3><br /> -<br /> -<span>The Application manifests serve as documentation:</span><br /> -<br /> -<ul> -<li>Which Helm chart version is deployed? → Check <span class='inlinecode'>targetRevision</span></li> -<li>What custom values are configured? → Check <span class='inlinecode'>valuesObject</span></li> -<li>Which namespace does this deploy to? → Check <span class='inlinecode'>destination.namespace</span></li> -<li>Is auto-sync enabled? → Check <span class='inlinecode'>syncPolicy.automated</span></li> -</ul><br /> -<span>No more guessing or checking <span class='inlinecode'>helm list</span> output.</span><br /> -<br /> -<h3 style='display: inline' id='7-safe-experimentation'>7. Safe Experimentation</h3><br /> -<br /> -<span>Create a feature branch, make changes, and preview them:</span><br /> -<br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ git checkout -b test-prometheus-upgrade -$ vim argocd-apps/prometheus.yaml <i><font color="silver"># Bump chart version</font></i> -$ git commit -am <font color="#808080">"Test Prometheus 56.0.0"</font> -$ git push origin test-prometheus-upgrade - -<i><font color="silver"># Temporarily point ArgoCD at the feature branch</font></i> -$ kubectl patch application prometheus -n cicd \ - --type merge \ - -p <font color="#808080">'{"spec":{"source":{"targetRevision":"test-prometheus-upgrade"}}}'</font> - -<i><font color="silver"># Verify changes in ArgoCD Web UI</font></i> -<i><font color="silver"># If good: merge to master</font></i> -<i><font color="silver"># If bad: revert the patch</font></i> +<span>$ argocd app rollback miniflux <revision-id></span><br /> +<pre> +### 5. Disaster Recovery + +If the entire cluster is destroyed, recovery is straightforward: + +1. Bootstrap a new k3s cluster +2. Create namespaces +3. Install ArgoCD +4. Apply all Application manifests: </pre> -<br /> -<h2 style='display: inline' id='challenges-and-solutions'>Challenges and Solutions</h2><br /> -<br /> -<h3 style='display: inline' id='challenge-1-helm-release-adoption'>Challenge 1: Helm Release Adoption</h3><br /> -<br /> -<span>When creating an Application for an existing Helm release, ArgoCD needs to "adopt" the resources. This failed initially with errors like:</span><br /> -<br /> +<span>$ kubectl apply -f argocd-apps/</span><br /> <pre> -The Helm operation failed with an error: release miniflux failed, and has been uninstalled due to atomic being set: timed out waiting for the condition +5. ArgoCD deploys all 21 applications to their desired state + +Total recovery time: ~30 minutes (mostly waiting for pods to pull images and start). + +### 6. Documentation by Default + +The Application manifests serve as documentation: + +* Which Helm chart version is deployed? → Check `targetRevision` +* What custom values are configured? → Check `valuesObject` +* Which namespace does this deploy to? → Check `destination.namespace` +* Is auto-sync enabled? → Check `syncPolicy.automated` + +No more guessing or checking `helm list` output. + +### 7. Safe Experimentation + +Create a feature branch, make changes, and preview them: + </pre> -<br /> -<span>**Solution**: For existing Helm releases, I first ensured the Application manifest matched the current Helm values exactly. ArgoCD then recognized the resources were already in the desired state and adopted them without re-deploying.</span><br /> -<br /> -<h3 style='display: inline' id='challenge-2-persistent-volumes-not-tracked-by-helm'>Challenge 2: Persistent Volumes Not Tracked by Helm</h3><br /> -<br /> -<span>PersistentVolumes are cluster-scoped resources, not namespace-scoped. Many of my Helm charts created PVs using <span class='inlinecode'>kubectl apply -f persistent-volumes.yaml</span> outside of Helm.</span><br /> -<br /> -<span>**Solution**: For simple apps, I moved the PV definitions into the Helm chart templates. For complex apps (like Prometheus), I used the multi-source pattern with PVs in the <span class='inlinecode'>manifests/</span> directory with sync wave 0.</span><br /> -<br /> -<h3 style='display: inline' id='challenge-3-secrets-management'>Challenge 3: Secrets Management</h3><br /> -<br /> -<span>ArgoCD stores Application manifests in Git, but secrets shouldn't be committed in plaintext.</span><br /> -<br /> -<span>**Solution (current)**: Secrets are created manually with <span class='inlinecode'>kubectl create secret</span> and referenced by the Helm charts. The secrets themselves aren't managed by ArgoCD.</span><br /> -<br /> -<span>**Future enhancement**: Migrate to External Secrets Operator (ESO) to manage secrets declaratively while storing the actual secrets in a separate backend (Kubernetes secrets in a separate namespace, or eventually Vault).</span><br /> -<br /> -<h3 style='display: inline' id='challenge-4-grafana-not-reloading-datasources'>Challenge 4: Grafana Not Reloading Datasources</h3><br /> -<br /> -<span>After updating the Grafana datasources ConfigMap, Grafana wouldn't detect the changes until pods were manually deleted.</span><br /> -<br /> -<span>**Solution**: Created a PostSync hook that automatically restarts Grafana pods after every ArgoCD sync. This runs as a Kubernetes Job in sync wave 10, ensuring it executes after all other resources are deployed.</span><br /> -<br /> -<h3 style='display: inline' id='challenge-5-prometheus-with-multiple-sources'>Challenge 5: Prometheus With Multiple Sources</h3><br /> -<br /> -<span>Prometheus needed both the upstream Helm chart and custom manifests (recording rules, dashboards, PVs).</span><br /> -<br /> -<span>**Solution**: Used ArgoCD's multi-source feature to combine:</span><br /> -<ul> -<li>Helm chart from <span class='inlinecode'>prometheus-community.github.io/helm-charts</span></li> -<li>Additional manifests from <span class='inlinecode'>codeberg.org/snonux/conf.git</span> at path <span class='inlinecode'>f3s/prometheus/manifests</span></li> -</ul><br /> -<span>This keeps the upstream chart cleanly separated from custom configuration.</span><br /> -<br /> -<h3 style='display: inline' id='challenge-6-sync-ordering-for-prometheus'>Challenge 6: Sync Ordering for Prometheus</h3><br /> -<br /> -<span>Prometheus resources have dependencies:</span><br /> -<ul> -<li>PVs before PVCs</li> -<li>Secrets before Prometheus Operator</li> -<li>PrometheusRule CRDs before Prometheus Operator can process them</li> -<li>Grafana must be running before the restart hook executes</li> -</ul><br /> -<span>**Solution**: Added sync wave annotations to all resources in <span class='inlinecode'>prometheus/manifests/</span>:</span><br /> -<ul> -<li>Wave 0: PVs, RBAC</li> -<li>Wave 1: Secrets, ConfigMaps</li> -<li>Wave 3: PrometheusRule CRDs (recording rules)</li> -<li>Wave 4: Dashboard ConfigMaps</li> -<li>Wave 10: PostSync hook (Grafana restart)</li> -</ul><br /> -<span>ArgoCD deploys resources in wave order, ensuring correct sequencing.</span><br /> -<br /> -<h2 style='display: inline' id='justfile-evolution'>Justfile Evolution</h2><br /> -<br /> -<span>The Justfiles evolved from deployment tools to utility scripts:</span><br /> -<br /> -<span>**Before (Helm deployment)**:</span><br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>install: - helm install miniflux ./helm-chart -n services +<span>$ git checkout -b test-prometheus-upgrade</span><br /> +<span>$ vim argocd-apps/prometheus.yaml # Bump chart version</span><br /> +<span>$ git commit -am "Test Prometheus 56.0.0"</span><br /> +<span>$ git push origin test-prometheus-upgrade</span><br /> +<br /> +<h1 style='display: inline' id='temporarily-point-argocd-at-the-feature-branch'>Temporarily point ArgoCD at the feature branch</h1><br /> +<span>$ kubectl patch application prometheus -n cicd \</span><br /> +<span> --type merge \</span><br /> +<span> -p '{"spec":{"source":{"targetRevision":"test-prometheus-upgrade"}}}'</span><br /> +<br /> +<h1 style='display: inline' id='verify-changes-in-argocd-web-ui'>Verify changes in ArgoCD Web UI</h1><br /> +<h1 style='display: inline' id='if-good-merge-to-master'>If good: merge to master</h1><br /> +<h1 style='display: inline' id='if-bad-revert-the-patch'>If bad: revert the patch</h1><br /> +<pre> +## Challenges and Solutions -upgrade: - helm upgrade miniflux ./helm-chart -n services +### Challenge 1: Helm Release Adoption + +When creating an Application for an existing Helm release, ArgoCD needs to "adopt" the resources. This failed initially with errors like: -uninstall: - helm uninstall miniflux -n services </pre> -<br /> -<span>**After (ArgoCD utilities)**:</span><br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>status: - @kubectl get pods -n services -l app=miniflux - @kubectl get application miniflux -n cicd -o jsonpath=<font color="#808080">'Sync: {.status.sync.status}, Health: {.status.health.status}'</font> +<span>The Helm operation failed with an error: release miniflux failed, and has been uninstalled due to atomic being set: timed out waiting for the condition</span><br /> +<pre> +Solution: For existing Helm releases, I first ensured the Application manifest matched the current Helm values exactly. ArgoCD then recognized the resources were already in the desired state and adopted them without re-deploying. -sync: - @kubectl annotate application miniflux -n cicd argocd.argoproj.io/refresh=normal --overwrite +### Challenge 2: Persistent Volumes Not Tracked by Helm -argocd-status: - argocd app get miniflux --core +PersistentVolumes are cluster-scoped resources, not namespace-scoped. Many of my Helm charts created PVs using `kubectl apply -f persistent-volumes.yaml` outside of Helm. -logs: - kubectl logs -n services -l app=miniflux --tail=<font color="#000000">100</font> -f +Solution: For simple apps, I moved the PV definitions into the Helm chart templates. For complex apps (like Prometheus), I used the multi-source pattern with PVs in the `manifests/` directory with sync wave 0. + +### Challenge 3: Secrets Management + +ArgoCD stores Application manifests in Git, but secrets shouldn't be committed in plaintext. + +Solution (current): Secrets are created manually with `kubectl create secret` and referenced by the Helm charts. The secrets themselves aren't managed by ArgoCD. + +Future enhancement: Migrate to External Secrets Operator (ESO) to manage secrets declaratively while storing the actual secrets in a separate backend (Kubernetes secrets in a separate namespace, or eventually Vault). + +### Challenge 4: Grafana Not Reloading Datasources + +After updating the Grafana datasources ConfigMap, Grafana wouldn't detect the changes until pods were manually deleted. + +Solution: Created a PostSync hook that automatically restarts Grafana pods after every ArgoCD sync. This runs as a Kubernetes Job in sync wave 10, ensuring it executes after all other resources are deployed. + +### Challenge 5: Prometheus With Multiple Sources + +Prometheus needed both the upstream Helm chart and custom manifests (recording rules, dashboards, PVs). + +Solution: Used ArgoCD's multi-source feature to combine: +* Helm chart from `prometheus-community.github.io/helm-charts` +* Additional manifests from `codeberg.org/snonux/conf.git` at path `f3s/prometheus/manifests` + +This keeps the upstream chart cleanly separated from custom configuration. + +### Challenge 6: Sync Ordering for Prometheus + +Prometheus resources have dependencies: +* PVs before PVCs +* Secrets before Prometheus Operator +* PrometheusRule CRDs before Prometheus Operator can process them +* Grafana must be running before the restart hook executes + +Solution: Added sync wave annotations to all resources in `prometheus/manifests/`: +* Wave 0: PVs, RBAC +* Wave 1: Secrets, ConfigMaps +* Wave 3: PrometheusRule CRDs (recording rules) +* Wave 4: Dashboard ConfigMaps +* Wave 10: PostSync hook (Grafana restart) + +ArgoCD deploys resources in wave order, ensuring correct sequencing. + +## Justfile Evolution + +The Justfiles evolved from deployment tools to utility scripts: + +Before (Helm deployment): </pre> +<span>install:</span><br /> +<span> helm install miniflux ./helm-chart -n services</span><br /> <br /> -<span>The Justfiles now provide:</span><br /> -<ul> -<li><span class='inlinecode'>status</span>: Quick health check</li> -<li><span class='inlinecode'>sync</span>: Force immediate ArgoCD sync (instead of waiting 3 minutes)</li> -<li><span class='inlinecode'>argocd-status</span>: Detailed ArgoCD application status</li> -<li><span class='inlinecode'>logs</span>: Tail application logs</li> -<li>Application-specific utilities (e.g., <span class='inlinecode'>port-forward</span>, <span class='inlinecode'>restart</span>)</li> -</ul><br /> -<h2 style='display: inline' id='lessons-learned'>Lessons Learned</h2><br /> -<br /> -<span>1. **Incremental migration is safer than big-bang**: Migrating one app at a time allowed me to validate the pattern and fix issues before they affected all apps.</span><br /> -<br /> -<span>2. **Start with simple apps**: The first migration (simple services) established the basic pattern. Complex apps (Prometheus) came later after the pattern was proven.</span><br /> -<br /> -<span>3. **Sync waves are essential for complex apps**: Without sync waves, resources deployed in random order and caused failures. Proper ordering eliminated all deployment issues.</span><br /> -<br /> -<span>4. **Multi-source is powerful**: Combining upstream Helm charts with custom manifests keeps configuration clean and maintainable.</span><br /> -<br /> -<span>5. **PostSync hooks replace manual steps**: The Grafana restart hook eliminated a manual step that was easy to forget.</span><br /> -<br /> -<span>6. **Documentation in Git is better than tribal knowledge**: The Application manifests document exactly what's deployed and how. No more "let me check my shell history to remember how I deployed this."</span><br /> -<br /> -<span>7. **Self-healing prevents configuration drift**: Multiple times I've manually tweaked something for debugging, forgotten about it, and ArgoCD automatically reverted it back to the desired state.</span><br /> -<br /> -<span>8. **ArgoCD Web UI is invaluable**: Seeing the resource tree, sync status, and health status at a glance is much better than running multiple <span class='inlinecode'>kubectl</span> commands.</span><br /> -<br /> -<h2 style='display: inline' id='future-improvements'>Future Improvements</h2><br /> -<br /> -<h3 style='display: inline' id='1-external-secrets-operator'>1. External Secrets Operator</h3><br /> -<br /> -<span>Currently, secrets are manually created with <span class='inlinecode'>kubectl create secret</span>. This works but isn't declarative. Plan:</span><br /> -<br /> -<ul> -<li>Deploy External Secrets Operator (ESO)</li> -<li>Store actual secrets in a Kubernetes Secret in a separate <span class='inlinecode'>secrets</span> namespace</li> -<li>Create ExternalSecret CRDs that reference the backend secrets</li> -<li>ArgoCD manages the ExternalSecret CRDs, ESO creates the actual Secrets</li> -</ul><br /> -<span>This makes secrets declarative while keeping them out of Git.</span><br /> -<br /> -<h3 style='display: inline' id='2-applicationset-for-similar-apps'>2. ApplicationSet for Similar Apps</h3><br /> -<br /> -<span>Many apps have nearly identical Application manifests (miniflux, freshrss, wallabag, etc.). ArgoCD ApplicationSets can generate multiple Applications from a template:</span><br /> +<span>upgrade:</span><br /> +<span> helm upgrade miniflux ./helm-chart -n services</span><br /> <br /> +<span>uninstall:</span><br /> +<span> helm uninstall miniflux -n services</span><br /> <pre> -apiVersion: argoproj.io/v1alpha1 -kind: ApplicationSet -metadata: - name: simple-services - namespace: cicd -spec: - generators: - - list: - elements: - - app: miniflux - - app: freshrss - - app: wallabag - template: - metadata: - name: '{{app}}' - spec: - project: default - source: - repoURL: https://codeberg.org/snonux/conf.git - targetRevision: master - path: 'f3s/{{app}}/helm-chart' - destination: - server: https://kubernetes.default.svc - namespace: services - syncPolicy: - automated: - prune: true - selfHeal: true +After (ArgoCD utilities): </pre> +<span>status:</span><br /> +<span> @kubectl get pods -n services -l app=miniflux</span><br /> +<span> @kubectl get application miniflux -n cicd -o jsonpath='Sync: {.status.sync.status}, Health: {.status.health.status}'</span><br /> <br /> -<span>One ApplicationSet could replace 10+ individual Application manifests.</span><br /> +<span>sync:</span><br /> +<span> @kubectl annotate application miniflux -n cicd argocd.argoproj.io/refresh=normal --overwrite</span><br /> <br /> -<h3 style='display: inline' id='3-app-of-apps-pattern'>3. App-of-Apps Pattern</h3><br /> -<br /> -<span>Currently, all Application manifests are applied manually with <span class='inlinecode'>kubectl apply -f argocd-apps/ -R</span>. An alternative is the "app-of-apps" pattern:</span><br /> -<br /> -<span>Create a root Application that deploys all other Applications. With the namespace-organized structure, this could be done per-namespace or for the entire cluster:</span><br /> +<span>argocd-status:</span><br /> +<span> argocd app get miniflux --core</span><br /> <br /> +<span>logs:</span><br /> +<span> kubectl logs -n services -l app=miniflux --tail=100 -f</span><br /> <pre> -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: root - namespace: cicd -spec: - source: - repoURL: https://codeberg.org/snonux/conf.git - targetRevision: master - path: f3s/argocd-apps - directory: - recurse: true # Recursively find all manifests in subdirectories - destination: - server: https://kubernetes.default.svc - namespace: cicd - syncPolicy: - automated: - prune: true - selfHeal: true +The Justfiles now provide: +* `status`: Quick health check +* `sync`: Force immediate ArgoCD sync (instead of waiting 3 minutes) +* `argocd-status`: Detailed ArgoCD application status +* `logs`: Tail application logs +* Application-specific utilities (e.g., `port-forward`, `restart`) + +## Lessons Learned + +1. Incremental migration is safer than big-bang: Migrating one app at a time allowed me to validate the pattern and fix issues before they affected all apps. + +2. Start with simple apps: The first migration (simple services) established the basic pattern. Complex apps (Prometheus) came later after the pattern was proven. + +3. Sync waves are essential for complex apps: Without sync waves, resources deployed in random order and caused failures. Proper ordering eliminated all deployment issues. + +4. Multi-source is powerful: Combining upstream Helm charts with custom manifests keeps configuration clean and maintainable. + +5. PostSync hooks replace manual steps: The Grafana restart hook eliminated a manual step that was easy to forget. + +6. Documentation in Git is better than tribal knowledge: The Application manifests document exactly what's deployed and how. No more "let me check my shell history to remember how I deployed this." + +7. Self-healing prevents configuration drift: Multiple times I've manually tweaked something for debugging, forgotten about it, and ArgoCD automatically reverted it back to the desired state. + +8. ArgoCD Web UI is invaluable: Seeing the resource tree, sync status, and health status at a glance is much better than running multiple `kubectl` commands. + +## Future Improvements + +### 1. External Secrets Operator + +Currently, secrets are manually created with `kubectl create secret`. This works but isn't declarative. Plan: + +* Deploy External Secrets Operator (ESO) +* Store actual secrets in a Kubernetes Secret in a separate `secrets` namespace +* Create ExternalSecret CRDs that reference the backend secrets +* ArgoCD manages the ExternalSecret CRDs, ESO creates the actual Secrets + +This makes secrets declarative while keeping them out of Git. + +### 2. ApplicationSet for Similar Apps + +Many apps have nearly identical Application manifests (miniflux, freshrss, wallabag, etc.). ArgoCD ApplicationSets can generate multiple Applications from a template: + </pre> -<br /> -<span>Or create separate root apps per namespace:</span><br /> -<br /> +<span>apiVersion: argoproj.io/v1alpha1</span><br /> +<span>kind: ApplicationSet</span><br /> +<span>metadata:</span><br /> +<span> name: simple-services</span><br /> +<span> namespace: cicd</span><br /> +<span>spec:</span><br /> +<span> generators:</span><br /> +<span> - list:</span><br /> +<span> elements:</span><br /> +<span> - app: miniflux</span><br /> +<span> - app: freshrss</span><br /> +<span> - app: wallabag</span><br /> +<span> template:</span><br /> +<span> metadata:</span><br /> +<span> name: '{{app}}'</span><br /> +<span> spec:</span><br /> +<span> project: default</span><br /> +<span> source:</span><br /> +<span> repoURL: https://codeberg.org/snonux/conf.git</span><br /> +<span> targetRevision: master</span><br /> +<span> path: 'f3s/{{app}}/helm-chart'</span><br /> +<span> destination:</span><br /> +<span> server: https://kubernetes.default.svc</span><br /> +<span> namespace: services</span><br /> +<span> syncPolicy:</span><br /> +<span> automated:</span><br /> +<span> prune: true</span><br /> +<span> selfHeal: true</span><br /> <pre> -# root-monitoring.yaml -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: root-monitoring - namespace: cicd -spec: - source: - repoURL: https://codeberg.org/snonux/conf.git - targetRevision: master - path: f3s/argocd-apps/monitoring - destination: - server: https://kubernetes.default.svc - namespace: cicd - syncPolicy: - automated: - prune: true - selfHeal: true +One ApplicationSet could replace 10+ individual Application manifests. + +### 3. App-of-Apps Pattern + +Currently, all Application manifests are applied manually with `kubectl apply -f argocd-apps/ -R`. An alternative is the "app-of-apps" pattern: + +Create a root Application that deploys all other Applications. With the namespace-organized structure, this could be done per-namespace or for the entire cluster: + </pre> -<br /> -<span>Then disaster recovery becomes:</span><br /> -<!-- Generator: GNU source-highlight 3.1.9 -by Lorenzo Bettini -http://www.lorenzobettini.it -http://www.gnu.org/software/src-highlite --> -<pre>$ kubectl apply -f root-app.yaml -<i><font color="silver"># Root app deploys all 21 applications automatically</font></i> +<span>apiVersion: argoproj.io/v1alpha1</span><br /> +<span>kind: Application</span><br /> +<span>metadata:</span><br /> +<span> name: root</span><br /> +<span> namespace: cicd</span><br /> +<span>spec:</span><br /> +<span> source:</span><br /> +<span> repoURL: https://codeberg.org/snonux/conf.git</span><br /> +<span> targetRevision: master</span><br /> +<span> path: f3s/argocd-apps</span><br /> +<span> directory:</span><br /> +<span> recurse: true # Recursively find all manifests in subdirectories</span><br /> +<span> destination:</span><br /> +<span> server: https://kubernetes.default.svc</span><br /> +<span> namespace: cicd</span><br /> +<span> syncPolicy:</span><br /> +<span> automated:</span><br /> +<span> prune: true</span><br /> +<span> selfHeal: true</span><br /> +<pre> +Or create separate root apps per namespace: -<i><font color="silver"># Or apply by namespace</font></i> -$ kubectl apply -f root-monitoring.yaml -$ kubectl apply -f root-services.yaml -$ kubectl apply -f root-infra.yaml </pre> -<br /> -<h3 style='display: inline' id='4-argocd-image-updater'>4. ArgoCD Image Updater</h3><br /> -<br /> -<span>For applications with custom Docker images (like the registry, tracing-demo), ArgoCD Image Updater can automatically update the image tag in Git when a new image is pushed:</span><br /> -<br /> +<h1 style='display: inline' id='root-monitoringyaml'>root-monitoring.yaml</h1><br /> +<span>apiVersion: argoproj.io/v1alpha1</span><br /> +<span>kind: Application</span><br /> +<span>metadata:</span><br /> +<span> name: root-monitoring</span><br /> +<span> namespace: cicd</span><br /> +<span>spec:</span><br /> +<span> source:</span><br /> +<span> repoURL: https://codeberg.org/snonux/conf.git</span><br /> +<span> targetRevision: master</span><br /> +<span> path: f3s/argocd-apps/monitoring</span><br /> +<span> destination:</span><br /> +<span> server: https://kubernetes.default.svc</span><br /> +<span> namespace: cicd</span><br /> +<span> syncPolicy:</span><br /> +<span> automated:</span><br /> +<span> prune: true</span><br /> +<span> selfHeal: true</span><br /> <pre> -metadata: - annotations: - argocd-image-updater.argoproj.io/image-list: | - app=registry.f3s.buetow.org/miniflux:~^v - argocd-image-updater.argoproj.io/write-back-method: git +Then disaster recovery becomes: </pre> +<span>$ kubectl apply -f root-app.yaml</span><br /> +<h1 style='display: inline' id='root-app-deploys-all-21-applications-automatically'>Root app deploys all 21 applications automatically</h1><br /> <br /> -<span>When a new image <span class='inlinecode'>registry.f3s.buetow.org/miniflux:v2.1.0</span> is pushed, Image Updater automatically:</span><br /> -<span>1. Updates the Helm values in Git</span><br /> -<span>2. Commits the change</span><br /> -<span>3. ArgoCD syncs the new image</span><br /> -<br /> -<span>This creates a fully automated CI/CD pipeline.</span><br /> -<br /> -<h2 style='display: inline' id='summary'>Summary</h2><br /> -<br /> -<span>Migrating from imperative Helm deployments to declarative GitOps with ArgoCD transformed how I manage the f3s cluster:</span><br /> -<br /> -<span>**Before**:</span><br /> -<ul> -<li>Manual Helm commands for every change</li> -<li>No visibility into cluster state</li> -<li>Difficult to track what changed and when</li> -<li>Disaster recovery required rebuilding from memory/notes</li> -</ul><br /> -<span>**After**:</span><br /> -<ul> -<li>Git is the single source of truth</li> -<li>Automatic synchronization of changes</li> -<li>Complete audit trail in Git history</li> -<li>Drift detection and self-healing</li> -<li>Disaster recovery: deploy ArgoCD, apply Application manifests, done</li> -<li>Organized by namespace for clarity</li> -</ul><br /> -<span>The migration took several days spread over a few weeks, migrating one application at a time. The result is a more maintainable, reliable, and recoverable cluster.</span><br /> -<br /> -<span>All 21 applications are now managed via GitOps, with the configuration living in:</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/conf/src/branch/master/f3s'>codeberg.org/snonux/conf/f3s</a><br /> -<br /> -<span>The ArgoCD Application manifests are organized by namespace:</span><br /> -<br /> -<a class='textlink' href='https://codeberg.org/snonux/conf/src/branch/master/f3s/argocd-apps'>codeberg.org/snonux/conf/f3s/argocd-apps</a><br /> -<br /> -<span>ArgoCD has become an essential part of the f3s infrastructure, and I can't imagine managing the cluster without it.</span><br /> -<br /> -<span>Other *BSD-related posts:</span><br /> -<br /> -<a class='textlink' href='./2025-12-07-f3s-kubernetes-with-freebsd-part-8.html'>2025-12-07 f3s: Kubernetes with FreeBSD - Part 8: Observability</a><br /> -<a class='textlink' href='./2025-10-02-f3s-kubernetes-with-freebsd-part-7.html'>2025-10-02 f3s: Kubernetes with FreeBSD - Part 7: k3s and first pod deployments</a><br /> -<a class='textlink' href='./2025-07-14-f3s-kubernetes-with-freebsd-part-6.html'>2025-07-14 f3s: Kubernetes with FreeBSD - Part 6: Storage</a><br /> -<a class='textlink' href='./2025-05-11-f3s-kubernetes-with-freebsd-part-5.html'>2025-05-11 f3s: Kubernetes with FreeBSD - Part 5: WireGuard mesh network</a><br /> -<a class='textlink' href='./2025-04-05-f3s-kubernetes-with-freebsd-part-4.html'>2025-04-05 f3s: Kubernetes with FreeBSD - Part 4: Rocky Linux Bhyve VMs</a><br /> -<a class='textlink' href='./2025-02-01-f3s-kubernetes-with-freebsd-part-3.html'>2025-02-01 f3s: Kubernetes with FreeBSD - Part 3: Protecting from power cuts</a><br /> -<a class='textlink' href='./2024-12-03-f3s-kubernetes-with-freebsd-part-2.html'>2024-12-03 f3s: Kubernetes with FreeBSD - Part 2: Hardware and base installation</a><br /> -<a class='textlink' href='./2024-11-17-f3s-kubernetes-with-freebsd-part-1.html'>2024-11-17 f3s: Kubernetes with FreeBSD - Part 1: Setting the stage</a><br /> -<a class='textlink' href='./2024-04-01-KISS-high-availability-with-OpenBSD.html'>2024-04-01 KISS high-availability with OpenBSD</a><br /> -<a class='textlink' href='./2024-01-13-one-reason-why-i-love-openbsd.html'>2024-01-13 One reason why I love OpenBSD</a><br /> -<a class='textlink' href='./2022-10-30-installing-dtail-on-openbsd.html'>2022-10-30 Installing DTail on OpenBSD</a><br /> -<a class='textlink' href='./2022-07-30-lets-encrypt-with-openbsd-and-rex.html'>2022-07-30 Let's Encrypt with OpenBSD and Rex</a><br /> -<a class='textlink' href='./2016-04-09-jails-and-zfs-on-freebsd-with-puppet.html'>2016-04-09 Jails and ZFS with Puppet on FreeBSD</a><br /> -<br /> -<span>E-Mail your comments to <span class='inlinecode'>paul@nospam.buetow.org</span></span><br /> -<br /> -<a class='textlink' href='../'>Back to the main site</a><br /> +<h1 style='display: inline' id='or-apply-by-namespace'>Or apply by namespace</h1><br /> +<span>$ kubectl apply -f root-monitoring.yaml</span><br /> +<span>$ kubectl apply -f root-services.yaml</span><br /> +<span>$ kubectl apply -f root-infra.yaml</span><br /> +<pre> +### 4. ArgoCD Image Updater + +For applications with custom Docker images (like the registry, tracing-demo), ArgoCD Image Updater can automatically update the image tag in Git when a new image is pushed: + +</pre> +<span>metadata:</span><br /> +<span> annotations:</span><br /> +<span> argocd-image-updater.argoproj.io/image-list: |</span><br /> +<span> app=registry.f3s.foo.zone/miniflux:~^v</span><br /> +<span> argocd-image-updater.argoproj.io/write-back-method: git</span><br /> <p class="footer"> Generated with <a href="https://codeberg.org/snonux/gemtexter">Gemtexter 3.0.1-develop</a> | served by <a href="https://www.OpenBSD.org">OpenBSD</a>/<a href="https://man.openbsd.org/relayd.8">relayd(8)</a>+<a href="https://man.openbsd.org/httpd.8">httpd(8)</a> | |
