<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Brett Ryan</title>
    <description>The latest articles on DEV Community by Brett Ryan (@brettryan).</description>
    <link>https://dev.to/brettryan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F589713%2F236d611b-9faf-4983-9074-541ebca9a7cc.png</url>
      <title>DEV Community: Brett Ryan</title>
      <link>https://dev.to/brettryan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brettryan"/>
    <language>en</language>
    <item>
      <title>Wabe Labs Is Born, and Building With Blueprints</title>
      <dc:creator>Brett Ryan</dc:creator>
      <pubDate>Thu, 10 Sep 2026 05:07:14 +0000</pubDate>
      <link>https://dev.to/brettryan/wabe-labs-is-born-and-building-with-blueprints-36ci</link>
      <guid>https://dev.to/brettryan/wabe-labs-is-born-and-building-with-blueprints-36ci</guid>
      <description>&lt;p&gt;Entering the third week of my startup adventure has been a deeply rewarding time of solidifying foundations. Things come together to form an official brand, I put together tools to help with financial discipline, and I finished off the week by practicing my own Documentation-as-a-Contract (DaaC) framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Have a Name: Wabe Labs
&lt;/h2&gt;

&lt;p&gt;While I have an idea that I truly believe in and know exactly the components I need to build, I have been feeling excited about this venture before starting. But one question that would keep entering my mind, &lt;em&gt;"What do you tell people your business is called?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've been following along, you know that I value structure. My development usually revolves around a folder structure on my laptop (&lt;code&gt;~/Develop/${company}/${project}&lt;/code&gt;), and for years I used &lt;code&gt;drunkendev&lt;/code&gt; for my personal projects, my original (un)professional handle. For this, I wanted a mature placeholder so had been using &lt;code&gt;new-biz&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For creating individual project names, I looked back to a tool I wrote years ago that generated secure passwords by pulling random words from Lewis Carroll’s &lt;em&gt;Alice in Wonderland&lt;/em&gt; texts. It would generate some quirky password combinations and is fondly remembered for making people laugh. I was using this method to define names, with the help of AI I would ask "Could you suggest some names for a core software engine using words from Lewis Carroll's Alice in Wonderland books?"&lt;/p&gt;

&lt;p&gt;From this method, I codenamed my central project &lt;strong&gt;project-wabe&lt;/strong&gt;. From then on, when I queried Gemini Notebook for architectural advice, it referenced "wabe" in its responses &lt;em&gt;"within wabe you..."&lt;/em&gt; After bouncing the concept off a group of former colleagues and receiving positive feedback, the choice was clear.&lt;/p&gt;

&lt;p&gt;In &lt;em&gt;Through the Looking-Glass&lt;/em&gt;, &lt;strong&gt;wabe&lt;/strong&gt; refers to the grass-plot surrounding a sundial, so called because it extends a long way before it, behind it, and on each side. For us, it represents the ideal architectural metaphor: defining the clear space and precise boundaries around modern software systems. You can read the full story on our &lt;a href="https://wabelabs.com/behind-the-name/" rel="noopener noreferrer"&gt;Behind the Name&lt;/a&gt; page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Securing the Digital Real Estate
&lt;/h3&gt;

&lt;p&gt;With the name chosen, the immediate next step was securing the online footprint. Fortunately, &lt;code&gt;wabelabs.com&lt;/code&gt; was available at retail pricing, so I secured it immediately after a quick name availability check in Australia. Once the business registration was finalised, I also purchased &lt;code&gt;wabelabs.com.au&lt;/code&gt; to be used as a redirect back to our main site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making it Official
&lt;/h3&gt;

&lt;p&gt;To make the brand official, I activated a sole-trader Australian Business Number (ABN) and registered the business name through ASIC.&lt;/p&gt;

&lt;p&gt;Having secured the name I thought &lt;em&gt;"Should I update my license headers from Brett Anthony Ryan?"&lt;/em&gt; I did a bit of sleuthing and learned a valuable legal lesson. As a sole trader, the business name is an operational identifier rather than a separate legal entity (which only comes with full incorporation as a Pty Ltd). For now, my personal name remains the contracting entity for my licenses and agreements, making this step both an operational milestone and a reality check on pre-incorporation setup.&lt;/p&gt;

&lt;p&gt;While it's true this is a simple step, completing this registration was a psychological turning point. Before taking the leap, there was always a lingering question: &lt;em&gt;"Am I really going to become a founder?"&lt;/em&gt; Securing the name was a concrete declaration of commitment: &lt;em&gt;This is it, I'm doing it.&lt;/em&gt; It shifted my mindset from &lt;em&gt;"I'm attempting it"&lt;/em&gt; to &lt;em&gt;"I'm doing it"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With the name established, I created the &lt;a href="https://www.linkedin.com/company/wabelabs" rel="noopener noreferrer"&gt;Wabe Labs LinkedIn page&lt;/a&gt;, published an &lt;a href="https://wabelabs.com/" rel="noopener noreferrer"&gt;elegant landing page on &lt;code&gt;wabelabs.com&lt;/code&gt;&lt;/a&gt;, and launched the &lt;a href="https://wabelabs.com/behind-the-name/" rel="noopener noreferrer"&gt;Behind the Name&lt;/a&gt; feature content.&lt;/p&gt;




&lt;h2&gt;
  
  
  Financial Discipline from Day One
&lt;/h2&gt;

&lt;p&gt;As I prepared my personal taxes for the end of FY26, it prompted me to think about establishing financial operations for Wabe Labs.&lt;/p&gt;

&lt;p&gt;So I set to, and built a structured Google Sheets workbook tracking income, expenses, and a stacked ledger with standardised file-naming patterns for instant cross-referencing. Opening a dedicated business bank account completely separated my personal and business finances.&lt;/p&gt;

&lt;p&gt;I have since run this setup by my accountant for review; having clean, audit-ready records from week three will make tax compliance and financial reporting effortless down the line.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practicing Documentation-as-a-Contract
&lt;/h2&gt;

&lt;p&gt;In my previous post, I detailed our &lt;a href="https://www.brettryan.dev/posts/week-two-hand-crafted-ai-advisor/" rel="noopener noreferrer"&gt;Documentation-as-a-Contract AI Advisor Network&lt;/a&gt;, our strategy for establishing boundaries, using AI as an architectural advisor rather than a code generator. For this past week I really put this philosophy to the test while architecting what will become the core functional component that makes up our offering.&lt;/p&gt;

&lt;p&gt;My exercise was to explore the best format for structured &lt;a href="https://mermaid.ai/open-source/" rel="noopener noreferrer"&gt;Mermaid&lt;/a&gt; diagrams for optimal AI context parsing.  By loading Mermaid documentation alongside our project requirements into Gemini Notebook, I established a constrained, high-fidelity context window.&lt;/p&gt;

&lt;p&gt;Through my interactions I got the warm and fuzzies as I realised, everything I've been preaching about creating constrained context windows for AI's created a precise hyper-focussed interaction that gives a much higher quality output. I had the feedback-loop model proved to be working as I iterated on my documentation.&lt;/p&gt;

&lt;p&gt;What emerged was a project that ticked all the boxes I was after:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Contracts (&lt;code&gt;API.md&lt;/code&gt;)&lt;/strong&gt;: a project &lt;code&gt;wabe-api-contract&lt;/code&gt; houses the external view of the projects gRPC and Protobuf service definitions that are published to my maven repository with java bindings. Auto-generated by &lt;a href="https://github.com/pseudomuto/protoc-gen-doc" rel="noopener noreferrer"&gt;protoc-gen-doc&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Interface&lt;/strong&gt;: Pure Java interfaces and records defining immutable domain schemas. Right now, this isn't auto-generated, but the other way around; it's all been defined in my &lt;code&gt;README.md&lt;/code&gt; that I used to create the project, and I may lift the UML documents into this document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Bridge&lt;/strong&gt;: Performing the bindings between model representation and wire representation, no project will need to do this on its own. Keeping it out of interface meant that interface remains pure, and keeping it out of contracts means that contracts has no transient dependency on the java records and interfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pulling this together felt extremely satisfying. In a world where so many are getting AI to generate code for them, I'm taking a completely different approach that may seem strange and less efficient, but I argue, that right now, I know exactly what my software is doing, how it's structured, and that structure came directly from me. I definitely used help, but the AI was helping me, not me instructing the AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to Next?
&lt;/h2&gt;

&lt;p&gt;Now that my foundations are stable, I'm left to concentrate on the build of my core project &lt;strong&gt;Gyre&lt;/strong&gt;: our feature execution engine.&lt;/p&gt;

&lt;p&gt;It's still funny thinking about it. All my career I've worked inside an established enterprise building with networks, VPN's with cloud providers wired in a hub-and-spoke architecture. Now, I'm starting with a clean slate, exploring how to build that brick and mortar foundation myself.&lt;/p&gt;

&lt;p&gt;As I think about how I might interact with Gyre running in my cloud environment, I think "Will I need to set-up an AWS Client VPN and Route 53?" Knowing that this can have a minimum cost of $100 a month, I'm looking for cheaper alternatives, and I think I might have discovered that with my current VPN provider, I might have a way with NordVPN's Meshnet to be able to provide this same capability for only a few dollars of additional AWS components.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>documentation</category>
      <category>startup</category>
    </item>
    <item>
      <title>Mind Discipline: Why Our AI Advisor Only Reads Hand-Crafted Contracts</title>
      <dc:creator>Brett Ryan</dc:creator>
      <pubDate>Fri, 28 Aug 2026 03:12:04 +0000</pubDate>
      <link>https://dev.to/brettryan/mind-discipline-why-our-ai-advisor-only-reads-hand-crafted-contracts-4n5a</link>
      <guid>https://dev.to/brettryan/mind-discipline-why-our-ai-advisor-only-reads-hand-crafted-contracts-4n5a</guid>
      <description>&lt;p&gt;In my first post, I wrote about why I spent my first week writing zero business logic and instead built &lt;strong&gt;rig&lt;/strong&gt; - our lightweight, POSIX-compliant local provisioning tool. It was my way of rejecting "wiki-ops" and applying Infrastructure-as-Code (IaC) discipline to our local environments so that a hardware failure means minutes of downtime, not a week.&lt;/p&gt;

&lt;p&gt;But as I transitioned into Week Two, I was hit by a different kind of operational reality check. &lt;/p&gt;

&lt;p&gt;For years, I had been building a comprehensive repository of system architecture, design decisions, and guidelines on Confluence. It was my digital home. So, knowing I would be creating a startup, I set to work writing my documentation in my spare time in preparation. But during a brief hiatus of inactivity, the space was silently, unceremoniously deleted. It was gone. Late nights of ideas, patterns, templates, and reference materials vanished into the cloud ether.&lt;/p&gt;

&lt;p&gt;That loss was a violent reminder of a lesson I thought I'd fully mastered: &lt;strong&gt;if your documentation doesn't live alongside your code, you don't truly own it.&lt;/strong&gt; Relying on third-party SaaS wikis to store the soul of your system architecture is just another form of "click-ops". It creates an artificial separation between the craftsmen writing the logic and the documentation that defines it.&lt;/p&gt;

&lt;p&gt;But rather than mourning my lost Confluence space, I treated it as a catalyst. I decided that our young startup would not have a bloated, detached corporate wiki. Instead, we would treat &lt;strong&gt;Documentation as a Contract&lt;/strong&gt; - a unified, git-backed human-and-machine contract that serves as the precise, zero-maintenance boundary for our AI systems.&lt;/p&gt;

&lt;p&gt;Here is how losing my documentation led to a new architectural philosophy, and how we built a zero-overhead, "Anti-AI AI Strategy" that uses GitLab CI/CD and Google Workspace to run a secure, managed RAG pipeline.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Anti-AI Strategy: Why We Refuse to Let AI Write Our Code
&lt;/h3&gt;

&lt;p&gt;Walk into almost any tech startup today, and you’ll find developers blindly feeding raw codebases into LLMs, asking them to write entire features from scratch. The result is a flood of low-effort, AI-generated slop, code that looks functional on the surface but lacks architectural cohesion, contains silent regressions, and strips away the engineering "fingerprint" of the creator.&lt;/p&gt;

&lt;p&gt;We refuse to work that way.&lt;/p&gt;

&lt;p&gt;To scale with pride and precision, our engineering philosophy is built on a simple premise: &lt;strong&gt;human intent over AI generation.&lt;/strong&gt; Great software is a work of hand-crafted art. The code is merely an implementation detail, a reflection of the craftsman's pride. It's so much fun when you hear a peer say "I just read your git-commit and it made me laugh".&lt;/p&gt;

&lt;p&gt;We do not use AI as a quick syntax completion engine or a mindless pull-request generator. Instead, we treat AI as a &lt;strong&gt;Principal Architect and Advisor&lt;/strong&gt;. It is our sparring partner for system design, edge case validation, and macro-architecture. But if the AI is to be an effective advisor, it needs clean, high-fidelity context. Feeding it a massive, messy codebase results in noisy context, token bloat, and tactical code generation instead of strategic guidance.&lt;/p&gt;

&lt;p&gt;This is where the concept of &lt;strong&gt;Documentation-as-Contract&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;Before a single line of business logic is written, the engineer must define the boundaries of the component. They do this by drafting several markdown files directly within the project's repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;README.md&lt;/strong&gt;: Describes the component’s internal mechanisms, local setup, and core intent. It explains the &lt;em&gt;why&lt;/em&gt; and the &lt;em&gt;how&lt;/em&gt; for human engineers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INTERFACE.md&lt;/strong&gt;: Describes the public-facing boundaries. This includes the  public-facing code interfaces (interfaces, abstract classes, records).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API.md&lt;/strong&gt;: Describes exposed ReST endpoints, gRPC/Protobuf contracts, GraphQL schemas.&lt;/li&gt;
&lt;li&gt;Any other content may be produced that may add or separate boundaries. For example, &lt;strong&gt;USAGE.md&lt;/strong&gt; might make more sense in some cases than creating a massive &lt;strong&gt;README.md&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Infrastructure as Code may also live in &lt;code&gt;infra/README.md&lt;/code&gt; as appropriate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This documentation is the &lt;strong&gt;source of truth&lt;/strong&gt;. If a public endpoint or module contract isn't documented in the &lt;code&gt;API.md&lt;/code&gt; file or the interface contracts are not exposed in &lt;code&gt;INTERFACE.md&lt;/code&gt;, then as far as the rest of the system (and our AI advisor) is concerned, &lt;strong&gt;it does not exist&lt;/strong&gt;. By forcing ourselves to write the interface first, we ensure deep clarity of thought before code execution. I've said it so many times to peers - If you can't explain the contract clearly in text, the code behind it is probably too complex, or worse, it's not fully understood.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two-Tiered Context Architecture (Zero-Maintenance RAG)
&lt;/h2&gt;

&lt;p&gt;You've probably seen the posts on Linkedin before, "How I used AI to create a RAG with 10 different pipeline components". It doesn't need to be so complex, and; you can do it with ease.&lt;/p&gt;

&lt;p&gt;I originally subscribed to a Google Workspaces Business account just so I could get guaranteed private conversation history with Google Gemini, but then I started using Gemini Notebook (formerly NotebookLM). You can create a precise context window by adding just the sources you need when you interact. Either direct web sources, or; documents directly from Google Drive which auto-update as the document updates.&lt;/p&gt;

&lt;p&gt;This prompted me the idea "Can I load my git-project documentation loaded to google workspaces as part of my CI/CD process?" This is where I took my concept of &lt;strong&gt;Documentation as a Contract&lt;/strong&gt; and applied it directly to my AI strategy.&lt;/p&gt;

&lt;p&gt;As part of this journey I set out a topology that centralises on a shared Google Workspace Drive called "Architecture". The primary consumer of this is my AI tools (Gemini Notebook), if it exists in the company, documentation must be stored here to back it. This is the knowledge repository, structured under automated folders mirroring the Gitlab project group path, and files named with the project embedded in them - this was a small lesson early on as it was hard to search, and differentiate sources when they all say &lt;code&gt;README.md&lt;/code&gt; within Google Drive.&lt;/p&gt;

&lt;p&gt;AI context is now surgically selected for each interaction by the author, they will select related components, either their &lt;code&gt;API.md&lt;/code&gt; or &lt;code&gt;INTERFACE.md&lt;/code&gt; depending on their interaction patterns, and may select relevant &lt;code&gt;README.md&lt;/code&gt; for components that the AI needs a deeper understanding - maybe they are working on that component.&lt;/p&gt;

&lt;p&gt;This approach allows Gemini to behave as both an architectural advisor and a project specialist for a given interaction. Since the author is selecting the precise components required - which are auto-updated, they get clear context with no overload and zero risk of the AI getting bogged down in needing to interpret low-level code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter &lt;code&gt;wabe-tools&lt;/code&gt;: Automating the Pipeline with Zero Overhead
&lt;/h2&gt;

&lt;p&gt;With a separation of concerns in-place, without closing the automation loop, it would fail. This is why I created a small script to turn a Markdown file into a native gdoc file and have it uploaded to Google Drive. Combining this with a Gitlab component that can be included in any project to upload the documentation and we've completely closed the loop from architect to developer and back to our AI advisor network.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;md2gdoc&lt;/code&gt;: The Google Cloud Document Loader
&lt;/h3&gt;

&lt;p&gt;Firstly I needed a way to get content into google docs. They didn't have to be beautiful, but good enough for Gemini to get full context. I spent a day writing a python script with the following key requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Must create a native Google Document file (gdoc)&lt;/li&gt;
&lt;li&gt;Must create paths within Google Drive to the document&lt;/li&gt;
&lt;li&gt;Must be able to specify a shared drive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the tool comes to the following contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;md2gdoc [-h] [--title TITLE] [--drive DRIVE] [--folder FOLDER] file`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/th&gt;
&lt;th&gt;Description &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;file&lt;/code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Path to input Markdown/Text file&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--title TITLE&lt;/code&gt; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Title for the Google Doc&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--drive DRIVE&lt;/code&gt; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Drive name to upload to.&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--folder FOLDER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Folder path within drive. &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It turns out that if you send a text file to Google Drive with the &lt;code&gt;mimeType&lt;/code&gt; set to &lt;code&gt;application/vnd.google-apps.document&lt;/code&gt;, the API automatically creates the google document and fills it with the text you pass, though; I am using &lt;a href="https://github.com/Significant-Gravitas/gravitas-md2gdocs" rel="noopener noreferrer"&gt;gravitas-md2gdocs&lt;/a&gt; for basic formatting.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;publish-docs&lt;/code&gt;: Gitlab Component to Close the Loop
&lt;/h3&gt;

&lt;p&gt;Tying it all together is a Gitlab component that when included in a &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; pipeline will execute &lt;code&gt;md2gdocs&lt;/code&gt; for each of the supplied documents. Sane defaults already look for the &lt;code&gt;README.md&lt;/code&gt; file, but these may also be overridden.&lt;/p&gt;

&lt;h4&gt;
  
  
  Inputs
&lt;/h4&gt;

&lt;p&gt;Inputs can be provided at the root and override-able at document item levels. Defaults are defined for our company documentation best practices that have been designed for our Gemini Notebook integration.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/th&gt;
&lt;th&gt;Description&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;stage&lt;/code&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Pipeline stage to run job in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;drive&lt;/code&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Target Google Shared Drive name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;folder&lt;/code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Target folder path in Drive. &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docs&lt;/code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;JSON string array of document objects.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docs.file&lt;/code&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;File to be sent to google drive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docs.title&lt;/code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Google document title.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs.full_title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allows providing a title that does not include the project name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docs.drive&lt;/code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Item level drive override. &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docs.folder&lt;/code&gt; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;td&gt;Item level folder override.&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Usage
&lt;/h4&gt;

&lt;p&gt;For a minimal implementation that publishes your project's &lt;code&gt;README.md&lt;/code&gt; to our central &lt;code&gt;Architecture&lt;/code&gt; as &lt;code&gt;Components/$CI_PROJECT_PATH/$CI_PROJECT_NAME :: README&lt;/code&gt;, the following default configuration satisfies this requirement for most projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;&amp;nbsp; - component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_SERVER_FQDN/my-group/wabe-tools/publish-docs@main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;infra-bootstrap-gcp&lt;/code&gt;: Granting Projects to Publish Documentation
&lt;/h3&gt;

&lt;p&gt;From the get-go, I knew I wanted to be able to send documentation directly from my laptop for testing purposes, but more importantly, I wanted this to be executed directly from my CI/CD pipeline. This is where I came up with the infrastructure required to grant individual projects and/or Gitlab groups Attribute Based Access Control (ABAC) least privileged to Google's API's via OpenID Connect (OIDC).&lt;/p&gt;

&lt;blockquote&gt;
&lt;h5&gt;
  
  
  Temporary friction gives the calluses to be stronger!
&lt;/h5&gt;

&lt;p&gt;It's at this point where I felt friction with the Google Cloud API's and Identity and Access management (IAM) differences between Amazon Web Services (AWS) a point of friction, though; having been through it has given me broader understanding that will help me later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demonstrating the same Documentation-as-Contract standards here, the following is an extract of the inputs/outputs from the &lt;code&gt;infra-bootstrap-gcp&lt;/code&gt; OpenTofu inputs/outputs section:&lt;/p&gt;

&lt;h4&gt;
  
  
  Inputs
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input_gitlab_group_ids&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List of groups that are granted access. Note: this is a direct ancestor to a project and ancestors of a direct group to a project are not supported.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input_gitlab_project_ids&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List of projects that are allowed access to write documents.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input_gitlab_root_project_path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path prefix for projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input_gitlab_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitLab URL used for the gitlab audience policy statement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Outputs
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google_organisation_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;output_google_pool_provider_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google Pool provider name used for the audience. GCP_WORKLOAD_IDENTITY_PROVIDER is to be populated with this value in the Gitlab CI/CD module which will create an audience that contains this value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;output_google_project_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;output_google_project_number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;output_google_service_account_email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google Service account the Gitlab CI/CD module will use to impersonate requests. GCP_SERVICE_ACCOUNT_EMAIL is to be populated with this value.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Documentation-as-Contract Pillars for AI and Humans
&lt;/h2&gt;

&lt;p&gt;Combining principals of &lt;a href="https://www.writethedocs.org/guide/docs-as-code/" rel="noopener noreferrer"&gt;Documentation as Code&lt;/a&gt;, Contract-Driven Development and Context Window Engineering, we have established the 5 pillars that make up this framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation is the Source of Truth&lt;/strong&gt;: Code becomes a downstream implementation of a well-defined written design. If it isn't documented it doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Context Overload&lt;/strong&gt;: Surgically tailored context windows for our AI, maximising the quality of Gemini’s strategic advice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD-Driven Architecture&lt;/strong&gt;: We establish trust by providing the tools to ensure that documentation adjacent to code, is sent to our centralised documentation store only when code piplines succeed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prototyping Without Code&lt;/strong&gt;: We can write, refine, and merge documentation first to validate an architectural idea or prototype before committing resources to build it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plumbing Code Auto-Documentation&lt;/strong&gt;: For repetitive plumbing (like Terraform configurations), we use GitLab components like &lt;code&gt;terraform-docs&lt;/code&gt; to auto-generate markdown files from code. This reverses the contract for the mundane, ensuring the plumbing documentation is always perfectly in sync.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  RAD is a Discipline, Not a Luxury Suite
&lt;/h2&gt;

&lt;p&gt;Many believe that Rapid Application Development (RAD) requires expensive enterprise software suites (Confluence, Jira, dedicated RAG search providers). Even I was fooled by my corporate discipline instilled in me over my career. With some forethought and discipline, you can build world-class developer experiences that are secure and scalable.&lt;/p&gt;

&lt;p&gt;Today my company's tech stack consists of AWS as my primary cloud provider serving the content you're likely reading from AWS CloudFront, a Google Workspaces integration with Google Drive and Gemini+Notebook, Gitlab CI/CD to stitch it all together and strict disciplines with a focus on repeatability such as Infrastructure as Code (IaC) and company tooling. I completely own the IP with a low cost of ownership. The highest hitter being my Google Workspaces license which is still reasonable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to From Here?
&lt;/h2&gt;

&lt;p&gt;With my tooling in place, my deployment pipelines running, and our core strategy solidified, I am finally ready to start building the heart of the business.&lt;/p&gt;

&lt;p&gt;As I begin writing our first services, my guiding architectural principle will be local-first, cloud-agnostic development. I want our applications to run flawlessly on a laptop with zero external dependencies, leveraging cloud-native features through clean facades and interfaces.&lt;/p&gt;

&lt;p&gt;This is where the twin rails of the last two weeks come together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;rig&lt;/strong&gt; will ensure that any developer tool, framework or cloud component needed for local-first development is instantly provisioned and standard across our environments with a single command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;publish-docs&lt;/strong&gt; will ensure that as we design our components, boundary contracts (&lt;code&gt;INTERFACE.md&lt;/code&gt; and &lt;code&gt;API.md&lt;/code&gt;) are immediately pushed to our central Workspace, allowing Gemini to act as our strategic sounding board for the next engineering phase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As central components, these will evolve over time, strengthening our base as we move forward.&lt;/p&gt;

&lt;p&gt;In my next post, I will share the journey of developing to contracts allows one to continue to be productive even if not connected to the internet, by utilising a local first testing and development strategy and share my continued story of the Documentation-as-a-Contract for our AI advisor.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Software Craftsman’s Day One: Why My First Week of Startup Life Had No Business Logic</title>
      <dc:creator>Brett Ryan</dc:creator>
      <pubDate>Mon, 24 Aug 2026 04:22:26 +0000</pubDate>
      <link>https://dev.to/brettryan/the-software-craftsmans-day-one-why-my-first-week-of-startup-life-had-no-business-logic-d2g</link>
      <guid>https://dev.to/brettryan/the-software-craftsmans-day-one-why-my-first-week-of-startup-life-had-no-business-logic-d2g</guid>
      <description>&lt;p&gt;Being made redundant from a position where I defined patterns and practices for a high-performance team, I decided to take the leap to create my own startup. As I write now, this has felt like a violent acceleration of the clock. One day you feel the safety of an established enterprise; the next, there I am, standing on a clean slate. When I finally received my new laptop last week, I felt the sudden urge to rush, install the software I needed, and start churning out lines of code.&lt;/p&gt;

&lt;p&gt;But my own discipline over the years took over - true craftsmanship rejects the illusion of hurried progress.&lt;/p&gt;

&lt;p&gt;Years ago, I learned a fundamental lesson in cloud architecture: &lt;strong&gt;refuse click-ops and always rely on Infrastructure as Code (IaC) as your source of truth&lt;/strong&gt;. Clicking through console windows to provision a database, where you instantly forget the exact steps you just took, builds technical debt that cannot be tracked, audited, or most crucially, reliably reproduced.&lt;/p&gt;

&lt;p&gt;I had actually meant to start one week prior, but as I received my refurbished laptop, there was an issue that meant I needed to return the machine for repair. This meant I had to wipe it and wait over a week for repairs.&lt;/p&gt;

&lt;p&gt;This taught me something: &lt;strong&gt;why not apply the same discipline of IaC to your own local environment?&lt;/strong&gt; If "click-ops" is a sin in the cloud, then clicking through manual installers and executing rogue &lt;code&gt;brew&lt;/code&gt; commands should be too.&lt;/p&gt;

&lt;p&gt;To scale with pride and precision, I spent my first week doing something counter-intuitive. I created a tool and ecosystem that separates the hardware from the environment using IaC &lt;strong&gt;principles&lt;/strong&gt; - designed to be liberating, not confining.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;The Onboarding Tax: How KT Nightmares Led to Local IaC&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The conviction to spend my first week writing zero business logic didn't come from a standard practice or theoretical textbook; it was forged in the exhausting final four months of my previous corporate role.&lt;/p&gt;

&lt;p&gt;My final four months was an exhausting Knowledge Transfer (KT) to an incoming team. They were talented engineers, but they had never worked within our company let alone our specific environment, and none of the legacy team would be remaining to guide them. I found myself trying to condense ten years of deep, institutional system knowledge into a compressed onboarding window.&lt;/p&gt;

&lt;p&gt;But the real bottleneck wasn’t teaching them our system design, it was the developer laptop environment.&lt;/p&gt;

&lt;p&gt;On paper, a new developer could get "up and running" in about a week. But that was a surface-layer illusion. For months afterward, they would constantly hit silent road-blocks. I’d find myself saying, &lt;em&gt;"Oh, you need to configure your SSH config this way,"&lt;/em&gt; or &lt;em&gt;"Right, you need this custom script to establish an AWS SSM tunnel to bypass that gateway."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Onboarding wasn't a single event; it was a slow, manual drip-feed of wiki pages, legacy scripts, and tribal knowledge. It was "wiki-ops" at its worst. What should have taken minutes took weeks, dragging down both the new engineers and the peers who had to constantly stop their own work to debug a local path.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Enter &lt;code&gt;rig&lt;/code&gt;: A Blueprint, Not a Cage&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To solve this onboarding tax permanently, I spent my first several days crafting &lt;strong&gt;&lt;code&gt;rig&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Named after the nautical verb &lt;em&gt;to equip a vessel with sails, standing tackle, and running gear&lt;/em&gt; - &lt;code&gt;rig&lt;/code&gt; is a lightweight, POSIX-compliant developer toolchain dispatcher and provisioning system. To ensure it runs everywhere without dependency hell, it is built strictly for &lt;code&gt;/bin/sh&lt;/code&gt;. This means it executes with zero pre-requisites across macOS, Linux distributions, and even within CI/CD runners.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  It doesn't support Linux, yet!
&lt;/h4&gt;

&lt;p&gt;While fully ready for Linux, without a functioning Linux system, I haven't yet tested it on Linux, and some of the scripts such as &lt;code&gt;brew&lt;/code&gt; haven't been configured with the different Linux distributions in mind, yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of a chaotic series of manual installers, setting up a new machine with our corporate standard is now a two-step process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/your-user/rig.git
&lt;span class="nb"&gt;cd &lt;/span&gt;rig &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./rig &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once executed, &lt;code&gt;rig&lt;/code&gt; injects a single clean hook into your shell profile (&lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.bashrc&lt;/code&gt;), marked clearly by &lt;code&gt;# rig-tools&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/rig-tools/rig"&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# rig-tools&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line evaluates a returned environment dynamically, allowing our developer tooling to evolve over time without having to re-install profile scripts.&lt;/p&gt;

&lt;p&gt;You might ask: &lt;strong&gt;Why not just use an industry-standard configuration manager like Chef or Puppet?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the enterprise space, tools like Chef or Puppet are often used to force compliance. They lock developers in a sterile box, dictating exactly how local machines must behave. It is an authoritarian approach to Developer Experience (DX) that strips away engineering personality and, frankly, makes talented developers want to quit. &lt;/p&gt;

&lt;p&gt;I wanted a tool that was &lt;strong&gt;elective, not prescriptive&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By leveraging a modular domain architecture (separating language runtimes from tools) and adhering strictly to XDG configuration standards (storing persistent, non-clobbering key-value states in &lt;code&gt;~/.config/rig/config&lt;/code&gt;), &lt;code&gt;rig&lt;/code&gt; acts as a launchpad, not a straightjacket. It guarantees every developer shares a baseline, reliable environment for our company's projects, while leaving them completely free to customise their editors, local paths, and shells.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# rig doctor ensures the environment hasn't drifted&lt;/span&gt;
rig doctor

&lt;span class="c"&gt;# Modular updates for languages and tools&lt;/span&gt;
rig update
rig lang java
rig tool vim obsidian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have turned what used to be weeks of peer-assisted debugging into a background task that takes minutes. If my laptop hardware fails tomorrow, I don’t face a week of downtime. I get a replacement machine, run &lt;code&gt;rig install&lt;/code&gt;, and I am back to writing code in under five minutes.&lt;/p&gt;

&lt;p&gt;Combine this with my standard rule &lt;strong&gt;"If you didn't commit and push your code to a branch, you didn't work today&lt;/strong&gt;", and we have an almost zero down-time developer productivity at the end of week 1.&lt;/p&gt;

&lt;p&gt;Once installed, a developer now has a useful tool (&lt;code&gt;rig&lt;/code&gt;), ready to help them perform tasks within the company. Its intended now that this will be extended for any repetitive tasks that I might find myself performing through my journey.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usage: rig &amp;lt;command&amp;gt; [options]

Commands:
  install
      Initiate the install script for the current platform
  update
      Update installed packages and toolchains
  doctor
      Verify environment
  tool &amp;lt;-l|tool&amp;gt;
      Configure/Install provided tool.
      Use "rig tool -l" to list available tools.

      Current: 1password, aws, colima, gcloud, git, localstack, obsidian,
               orbstack, tmux, vim

  lang &amp;lt;-l|lang&amp;gt;
      Configure/Install provided language support.
      Use "rig lang -l" to list available languages

      Current: java
  env
      Print environment include for use in shell scripts.
      NOTE: This will be added by 'install' to your environment, e.g.
            eval "$("/home/your-name/devel/rig-tools/rig" env)"
  config [-n] &amp;lt;key&amp;gt; [new-value]
      Get the current value or update the existing value with [new-value] if
      provided. The '-n' flag when present will not clobber the existing value.
  help
      Show this help

Config Values

  SKIP_INCLUDE  When this setting is 'true' the install/update/doctor commands
                will not attempt to add to your env, any other value will or
                when missing is treated as false.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where to From Here?
&lt;/h2&gt;

&lt;p&gt;Using these same fundamental principals, I've now been working on centralising my documentation strategy to function as what I'm calling an &lt;strong&gt;"Anti-AI AI Strategy"&lt;/strong&gt; - we value the human creativity, but empower the mind with the tools to perform their role.&lt;/p&gt;

&lt;p&gt;In my next post, I'll be talking about how I created a general contract for software projects to be documentation-first that encourages pipelines to centralise documentation in a shared company Google Drive (Architecture) with the dual-purpose goal of humans being able to centrally read the documentation, and more crucially; give Gemini Notebook the ability to have precise context on what the actor is interacting with the notebook for.&lt;/p&gt;

</description>
      <category>infrastructureascode</category>
      <category>devops</category>
      <category>startup</category>
    </item>
    <item>
      <title>Utility to Find Gitlab Credit Usage</title>
      <dc:creator>Brett Ryan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 01:40:20 +0000</pubDate>
      <link>https://dev.to/brettryan/utility-to-find-gitlab-credit-usage-59j4</link>
      <guid>https://dev.to/brettryan/utility-to-find-gitlab-credit-usage-59j4</guid>
      <description>&lt;p&gt;Through this article, we're going to explore the &lt;a href="https://docs.gitlab.com/cli/" rel="noopener noreferrer"&gt;GitLab CLI (glab)&lt;/a&gt;&lt;br&gt;
to help us query &lt;a href="https://docs.gitlab.com/ci/pipelines/compute_minutes/" rel="noopener noreferrer"&gt;compute minutes&lt;/a&gt; consumed and remaining.&lt;/p&gt;

&lt;p&gt;I created this helper script as I was iteratively making a lot of mistakes on my &lt;a href="https://about.gitlab.com/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt; runners which was eating into my free 400 minutes that's given when you sign up. I will now periodically run this to see how close I am and if I need to purchase additional credits.&lt;/p&gt;

&lt;p&gt;Creating our own tools and helpers not only save us time in the long-run, but they do way more than that, they teach us more than we would have ever known about what's available and give us incrementally acquired knowledge on many auxiliary tools and languages that we can flex throughout our careers.&lt;/p&gt;

&lt;p&gt;Once complete, the final output should look like the following&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current Date : 2026-05-25
Month Elapsed: 80%
Month Credits: 400

MONTH    CONSUMED  REMAINING  USAGE %
-----    --------  ---------  -------
2026-03       221        179      55%
2026-04       302         98      76%
2026-05       277        123      69%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Full Script
&lt;/h4&gt;

&lt;p&gt;You can obtain a copy of the &lt;a href="https://gitlab.com/-/snippets/5995177" rel="noopener noreferrer"&gt;full script&lt;/a&gt; which has been published as a public GitLab snippet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;Code snippets and scripts provided within this article are strictly for educational and informational purposes.  This code is provided "as-is" without any express or implied warranties of any kind. Always review and test scripts in a safe, non-production environment before deployment. See License Note for details on licensing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies
&lt;/h2&gt;

&lt;p&gt;Usage of the scripts within this article use two tools that are staples of a GitLab DevOps engineer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.gnu.org/software/bash/manual/" rel="noopener noreferrer"&gt;GNU Bash 4.0+&lt;/a&gt; is required as a shell interpreter. While I do make efforts to use 3.2 compatibility for macOS systems that still use the older shell, I do have a modern 5.x shell that I may have missed some compatibility, and I would encourage you to at least use a modern shell.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jqlang.org/" rel="noopener noreferrer"&gt;jq&lt;/a&gt; is an extremely useful JSON parser tool that allows transforming, navigating and producing output from structured JSON content. This should be a staple of any engineer who works with JSON structures without the need to open up a dependent programming language other than a shell.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/cli/" rel="noopener noreferrer"&gt;GitLab CLI (glab)&lt;/a&gt; provides an excellent Command Line Interface (CLI) to hosted and the cloud hosted GitLab platform. If you're a regular GitLab user, this should become a core part of your toolkit. NOTE: for those from a GitHub background, this can be thought of as the GitLab equivalent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Platform Compatibility
&lt;/h2&gt;

&lt;p&gt;While I personally use a macOS system with tooling provided by the &lt;a href="https://www.macports.org/" rel="noopener noreferrer"&gt;MacPorts&lt;/a&gt; project to remain current, I make best efforts to try and be platform independent by using POSIX standards and attempts at portability so that these scripts can be used in CI/CD platforms that may be running GLU/Linux. That said I can miss something, please use your best efforts to look for compatibility issues before attempting to run in different environments.&lt;/p&gt;

&lt;p&gt;Within this script one known portability is the difference in the &lt;a href="https://man.freebsd.org/cgi/man.cgi?date" rel="noopener noreferrer"&gt;BSD Date&lt;/a&gt; vs the &lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/Examples-of-date.html" rel="noopener noreferrer"&gt;GNU Date&lt;/a&gt;. The script artifact from this article takes this into account and makes an attempt to test the BSD version before the GNU version, you may choose to reverse this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the Problem?
&lt;/h2&gt;

&lt;p&gt;GitLab provides us with a screen that we can visually inspect our current and past usage history. You can find this by navigating to your group in your GitLab instance and looking under Settings → Usage Quotas and navigate to the "Pipelines" tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7woku88nqtcd3ysck6mk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7woku88nqtcd3ysck6mk.png" alt="GitLab Group Usage Quotas" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using this ClickOps&lt;sup id="fnref1"&gt;1&lt;/sup&gt; approach to finding information requires us to manually navigate and inspect. We would like to script this so that we as developers can get this information directly in a human friendly way, and; potentially add this to our CI/CD pipeline that could report this after each build.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitLab GraphQL
&lt;/h2&gt;

&lt;p&gt;We will be using the &lt;a href="https://docs.gitlab.com/api/graphql/reference/" rel="noopener noreferrer"&gt;GitLab GraphQL API&lt;/a&gt; to query our usage. One note on the GitLab documentation is a single-page huge document that contains the whole GraphQL documentation and for my system at least, takes a long time to load and searching is near impossible which made writing this article painful, your mileage may vary.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Git project directories influence authentication
&lt;/h4&gt;

&lt;p&gt;As taken from &lt;code&gt;glab api graphql --help&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the current directory is a Git directory, uses the GitLab authenticated host in the current directory. Otherwise, &lt;code&gt;gitlab.com&lt;/code&gt; will be used.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;We will be using the &lt;a href="https://docs.gitlab.com/api/graphql/reference/#queryciminutesusage" rel="noopener noreferrer"&gt;Query.ciMinutesUsage&lt;/a&gt; to query our usage. We can test this now by performing a query against the group namespace with the following replacing 9999 with your group id:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  How do I find my Group ID?
&lt;/h4&gt;

&lt;p&gt;Within the GitLab UI, you can find your Group ID by navigating to your group, click the three dots to the top right next to the "Create Project" button and choosing "Copy group ID" where it will also be displayed.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;glab api graphql &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{
  ciMinutesUsage(namespaceId: "gid://gitlab/Group/9999") {
    nodes {
      monthIso8601
      minutes
    }
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on if you have been using GitLab hosted runners, you should get something like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ciMinutesUsage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"monthIso8601"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;266&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"monthIso8601"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"monthIso8601"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;221&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could then put this in a script and call it a day, potentially doing basic manipulation to limit with 1, and grep for minutes and call it a day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9999

glab api graphql &lt;span class="nt"&gt;--output&lt;/span&gt; ndjson &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{
  ciMinutesUsage(namespaceId: "gid://gitlab/Group/'&lt;/span&gt;&lt;span class="nv"&gt;$gid&lt;/span&gt;&lt;span class="s1"&gt;'", first: 1) {
    nodes {
      monthIso8601
      minutes
    }
  }
}'&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.ciMinutesUsage.nodes[].minutes'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we introduced the &lt;code&gt;first&lt;/code&gt; query parameter where this GraphQL query supports pagination, we then used a basic &lt;code&gt;jq&lt;/code&gt; script to extract the minutes attribute. Since we selected a limit of the first 1 record from GitLab, this will always be a single element.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage History
&lt;/h2&gt;

&lt;p&gt;Showing the consumed capacity is good, but it doesn't tell us if we started consuming credits or, if that value is from the prior month. Let's look at using the &lt;code&gt;jq&lt;/code&gt; command to format this as a table that has the date and the number of consumed credits for that month.&lt;/p&gt;

&lt;p&gt;When we start to get to this sort of processing, I like to place the output of my command into a variable, and then use that variable to pass as the input to my &lt;code&gt;jq&lt;/code&gt; command. This gives two benefits.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's easier to read and reason about the output of a command and the input to the next command instead of relying on our cognitive capacity to recognise all those chained pipes.&lt;/li&gt;
&lt;li&gt;We can debug the output by echoing it so we can see when something is going awry (as it eventually will), allowing us to solve much faster.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9999

&lt;span class="nv"&gt;req&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{
  ciMinutesUsage(namespaceId: "gid://gitlab/Group/'&lt;/span&gt;&lt;span class="nv"&gt;$gid&lt;/span&gt;&lt;span class="s1"&gt;'") {
    nodes {
      monthIso8601
      minutes
    }
  }
}'&lt;/span&gt;
&lt;span class="nv"&gt;res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;glab api graphql &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$req&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# For debugging we can comment and un-comment as needed.&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"req: &lt;/span&gt;&lt;span class="nv"&gt;$req&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"res: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can take advantage of the &lt;a href="https://jqlang.org/manual/v1.5/#format-strings-and-escaping" rel="noopener noreferrer"&gt;Format strings and escaping&lt;/a&gt; &lt;code&gt;@tsv&lt;/code&gt; &lt;code&gt;jq&lt;/code&gt; built-in which allows us to take an array of fields and turn it into a tab separated table of values with each array being represented on a new line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  .data.ciMinutesUsage.nodes |
    sort_by(.monthIso8601)[] |
    [.monthIso8601, .minutes] |
    @tsv'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This now produces a nice tab-separated-values table for each month. Note that I've also reversed the sort by adding an explicit sort on &lt;code&gt;.monthIso8601&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="ld"&gt;2026-03-01&lt;/span&gt;  &lt;span class="mf"&gt;221&lt;/span&gt;
&lt;span class="ld"&gt;2026-04-01&lt;/span&gt;  &lt;span class="mf"&gt;302&lt;/span&gt;
&lt;span class="ld"&gt;2026-05-01&lt;/span&gt;  &lt;span class="mf"&gt;277&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is my Limit?
&lt;/h2&gt;

&lt;p&gt;Having a listing of what's consumed is very helpful in its own right, though; it would be really helpful if we could also display how much we have available for use.&lt;/p&gt;

&lt;p&gt;At the time of writing I could not find a GraphQL query that would return the credits available, but; I did find the &lt;code&gt;glab api /namespaces/$gid&lt;/code&gt; gives us what we need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DEFAULT_MONTH_CREDITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;400

&lt;span class="nv"&gt;ns_res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;glab api /namespaces/&lt;span class="nv"&gt;$gid&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ns_res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Couldn't find credits, defaulting to &lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_MONTH_CREDITS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;month_credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_MONTH_CREDITS&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ns_res&lt;/span&gt; |jq &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nv"&gt;blim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ns_res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.shared_runners_minutes_limit // '&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_MONTH_CREDITS&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;elim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ns_res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.extra_shared_runners_minutes_limit // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Base: &lt;/span&gt;&lt;span class="nv"&gt;$blim&lt;/span&gt;&lt;span class="s2"&gt;, Extra: &lt;/span&gt;&lt;span class="nv"&gt;$elim&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;month_credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;blim &lt;span class="o"&gt;+&lt;/span&gt; elim&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will first try to find what credits are associated, if returned the sum of &lt;code&gt;shared_runners_minutes_limit + extra_shared_runners_minutes_limit&lt;/code&gt; if they are available. Note that we use the &lt;a href="https://jqlang.org/manual/#alternative-operator" rel="noopener noreferrer"&gt;jq alternative operator&lt;/a&gt; which will return the left value if it's not null, otherwise defaults to the rightmost value.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Many Credits Remain?
&lt;/h2&gt;

&lt;p&gt;Now that we have the amount of credits consumed, and our credit limit; we can add a column to our table to show how many credits we have left for the month. This will show based on the current credit limit for the prior months.&lt;/p&gt;

&lt;p&gt;We will alter our script to ensure that we find available credits before the history table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DEFAULT_MONTH_CREDITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;400

&lt;span class="c"&gt;# Find monthly credits&lt;/span&gt;
&lt;span class="nv"&gt;ns_res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;glab api /namespaces/&lt;span class="nv"&gt;$gid&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ns_res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Couldn't find credits, defaulting to &lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_MONTH_CREDITS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;month_credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_MONTH_CREDITS&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;blim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ns_res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.shared_runners_minutes_limit // '&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_MONTH_CREDITS&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;elim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ns_res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.extra_shared_runners_minutes_limit // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Base: &lt;/span&gt;&lt;span class="nv"&gt;$blim&lt;/span&gt;&lt;span class="s2"&gt;, Extra: &lt;/span&gt;&lt;span class="nv"&gt;$elim&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;month_credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;blim &lt;span class="o"&gt;+&lt;/span&gt; elim&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Build table of remainders&lt;/span&gt;
&lt;span class="nv"&gt;req&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{
  ciMinutesUsage(namespaceId: "gid://gitlab/Group/'&lt;/span&gt;&lt;span class="nv"&gt;$gid&lt;/span&gt;&lt;span class="s1"&gt;'") {
    nodes {
      monthIso8601
      minutes
    }
  }
}'&lt;/span&gt;
&lt;span class="nv"&gt;res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;glab api graphql &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$req&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  .data.ciMinutesUsage.nodes |
    sort_by(.monthIso8601)[] |
    ('&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s1"&gt;' - .minutes) as $remaining |
    [
      .monthIso8601,
      .minutes,
      $remaining
    ] | @tsv'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we will have an output similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="nv"&gt;Base:&lt;/span&gt; &lt;span class="mf"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Extra:&lt;/span&gt; &lt;span class="mf"&gt;0&lt;/span&gt;
&lt;span class="ld"&gt;2026-03-01&lt;/span&gt;  &lt;span class="mf"&gt;221&lt;/span&gt; &lt;span class="mf"&gt;179&lt;/span&gt;
&lt;span class="ld"&gt;2026-04-01&lt;/span&gt;  &lt;span class="mf"&gt;302&lt;/span&gt; &lt;span class="mf"&gt;98&lt;/span&gt;
&lt;span class="ld"&gt;2026-05-01&lt;/span&gt;  &lt;span class="mf"&gt;277&lt;/span&gt; &lt;span class="mf"&gt;123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Note on passing values
&lt;/h4&gt;

&lt;p&gt;Note that in order for us to pass the &lt;code&gt;$month_credits&lt;/code&gt; variable value we had to close the quoted script and reopen after referring to the variable value. An alternative would have been to pass the variable as an argument to &lt;code&gt;jq&lt;/code&gt;, though; pay attention that it must be converted &lt;code&gt;tonumber&lt;/code&gt; in order to be used in math expressions.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; credits &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
  .data.ciMinutesUsage.nodes |
    sort_by(.monthIso8601)[] |
    (($credits | tonumber) - .minutes) as $remaining |
    [
      .monthIso8601,
      .minutes,
      $remaining
    ] | @tsv'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;For a nice effect, we can now add a percentage to our table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  .data.ciMinutesUsage.nodes |
    sort_by(.monthIso8601)[] |
    ('&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s1"&gt;' - .minutes) as $remaining |
    [
      .monthIso8601,
      .minutes,
      $remaining,
      ((100 * .minutes / '&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s1"&gt;' | round | tostring) + "%")
    ] | @tsv'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will give us a table that contains consumed, remaining and a usage %. Now it would be nice to have headings on this table so we don't have to remember what each column represents.&lt;/p&gt;

&lt;p&gt;In order to achieve this, we will update our script to contain two parts by using the &lt;a href="https://jqlang.org/manual/#comma" rel="noopener noreferrer"&gt;comma operator&lt;/a&gt; and a constant array for all our headings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  (["MONTH", "CONSUMED", "REMAINING", "USAGE %"]),
  (
    .data.ciMinutesUsage.nodes |
      sort_by(.monthIso8601)[] |
      ('&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s1"&gt;' - .minutes) as $remaining |
      [
        .monthIso8601,
        .minutes,
        $remaining,
        ((100 * .minutes / '&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s1"&gt;' | round | tostring) + "%")
      ]
  ) | @tsv'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run this, you will now find the columns are not aligned with the output. We can fix this by using the &lt;code&gt;column&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | column &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table doesn't look bad, though we might like to add underline separators from the heading to the data. To do this, we can use the comma operator again within the headings to split its output to the original value and repeat the &lt;code&gt;-&lt;/code&gt; character for the length of the heading. In the following modification, note we use the &lt;a href="https://jqlang.org/manual/#identity" rel="noopener noreferrer"&gt;identity&lt;/a&gt; to print the heading, then the comma operator, followed by a &lt;code&gt;map(length | . * "-")&lt;/code&gt;. What this is doing is mapping the heading to the length number and multiplying with a string literal which results in the repeated character.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  (["MONTH", "CONSUMED", "REMAINING", "USAGE %"] | (., map(length | . * "-"))),
  (
    .data.ciMinutesUsage.nodes |
      sort_by(.monthIso8601)[] |
      ('&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s1"&gt;' - .minutes) as $remaining |
      [
        .monthIso8601,
        .minutes,
        $remaining,
        ((100 * .minutes / '&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s1"&gt;' | round | tostring) + "%")
      ]
  ) | @tsv'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Much of the Month has Passed?
&lt;/h2&gt;

&lt;p&gt;While we now show percentage used, we could show a percentage of how far we are into the current month. This will allow us to visually see if we might be going over in the current month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%e&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;days_in_month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;+1m &lt;span class="nt"&gt;-v1d&lt;/span&gt; &lt;span class="nt"&gt;-v-1d&lt;/span&gt; +%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;month_elapsed_pct&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; day &lt;span class="o"&gt;/&lt;/span&gt; days_in_month&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Current Date : &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Month Elapsed: &lt;/span&gt;&lt;span class="nv"&gt;$month_elapsed_pct&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will work great, but; only on a BSD based system. The only way I could think to make this work for a GNU/Linux based system is to first fail with the BSD version and then try the GNU version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%e&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;days_in_month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;+1m &lt;span class="nt"&gt;-v1d&lt;/span&gt; &lt;span class="nt"&gt;-v-1d&lt;/span&gt; +%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;days_in_month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-01 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
      +1 month -1 day"&lt;/span&gt; +%d 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Could not determine find BSD or GNU compatible date command."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;3
  &lt;span class="k"&gt;fi
fi
&lt;/span&gt;&lt;span class="nv"&gt;month_elapsed_pct&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; day &lt;span class="o"&gt;/&lt;/span&gt; days_in_month&lt;span class="k"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tightening things up
&lt;/h2&gt;

&lt;p&gt;While we have something fairly useful that we can call at any time, the script is still fairly basic, and requires us to hard code values. We can first look at moving all the logic to function blocks before adding command line options to manipulate the scripts behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move Logic to Functions
&lt;/h2&gt;

&lt;p&gt;While probably unnecessary for this script, it's a practice I like to do as it allows me to turn features on and off easily, or move call order around if I want the percentage of month elapsed to appear first for example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;MONTH_CREDITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;400
&lt;span class="nv"&gt;MONTHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4

query_usage&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  glab api graphql &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{
    ciMinutesUsage(namespaceId: "gid://gitlab/Group/'&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s1"&gt;'", first: '&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s1"&gt;') {
      nodes {
        monthIso8601
        minutes
      }
    }
  }'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

find_credits&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;query_credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;ns_res elim blim

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; query_credits &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"Querying limits... "&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nv"&gt;ns_res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;glab api /namespaces/&lt;span class="nv"&gt;$gid&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ns_res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"Failed, falling back to defaults.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;else
      &lt;/span&gt;&lt;span class="nv"&gt;blim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ns_res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.shared_runners_minutes_limit // '&lt;/span&gt;&lt;span class="nv"&gt;$credits&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;elim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ns_res&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.extra_shared_runners_minutes_limit // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"Base: &lt;/span&gt;&lt;span class="nv"&gt;$blim&lt;/span&gt;&lt;span class="s2"&gt;, Extra: &lt;/span&gt;&lt;span class="nv"&gt;$elim&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="nv"&gt;credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;blim &lt;span class="o"&gt;+&lt;/span&gt; elim&lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;fi
  fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$credits&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

show_month_elapsed&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;days_in_month month_elapsed_pct
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%e&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;days_in_month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;+1m &lt;span class="nt"&gt;-v1d&lt;/span&gt; &lt;span class="nt"&gt;-v-1d&lt;/span&gt; +%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;days_in_month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-01 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; +1 month -1 day"&lt;/span&gt; +%d 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Could not determine find BSD or GNU compatible date command."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="nb"&gt;exit &lt;/span&gt;3
    &lt;span class="k"&gt;fi
  fi
  &lt;/span&gt;&lt;span class="nv"&gt;month_elapsed_pct&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; day &lt;span class="o"&gt;/&lt;/span&gt; days_in_month&lt;span class="k"&gt;))&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Current Date : &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Month Elapsed: &lt;/span&gt;&lt;span class="nv"&gt;$month_elapsed_pct&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

show_month_remaining&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
  &lt;span class="nv"&gt;api_res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;query_usage &lt;span class="nv"&gt;$gid&lt;/span&gt; 1&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$api_res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; credits &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$credits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
    .data.ciMinutesUsage.nodes |
      sort_by(.monthIso8601) |
      last |
      ($credits | tonumber) - .minutes
    '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

show_usage_history&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;months&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"Month Credits: &lt;/span&gt;&lt;span class="nv"&gt;$credits&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nv"&gt;api_res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;query_usage &lt;span class="nv"&gt;$gid&lt;/span&gt; &lt;span class="nv"&gt;$months&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;res&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$api_res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; credits &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$credits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
    # Helper function to pad strings for right alignment
    def lpad(n): tostring | ("          " + .) [-n:];

    (
      ["MONTH", "CONSUMED", "REMAINING", "USAGE %"] | (., map(length | . * "-"))
    ),
    (
      .data.ciMinutesUsage.nodes |
        sort_by(.monthIso8601)[] |
        ($credits | tonumber) as $credits |
        ($credits - .minutes) as $remaining |
        [
          (.monthIso8601 [:7]),
          (.minutes | lpad(8)),
          ($remaining | lpad(9)),
          ((100 * .minutes / $credits | round | tostring) + "%" | lpad(7))
        ]
    )
    | @tsv'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | column &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've tightened these up a little with some preparation for enabling some command line options. Now we can call each of these as we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;month_credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_credits &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MONTH_CREDITS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 0&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Remaining: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;show_month_remaining &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
show_month_elapsed
show_usage_history &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MONTHS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding Command Line Options
&lt;/h2&gt;

&lt;p&gt;Now that we have a modular approach, we can add some command line options that the user can pass to manipulate the scripts behaviour. We will provide the following options, which; we might as well write out in a help function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;show_help&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"usage: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; [options]

  -h            : Show this help.
  -c &amp;lt;credits&amp;gt;  : Monthly credits (default: &lt;/span&gt;&lt;span class="nv"&gt;$MONTH_CREDITS&lt;/span&gt;&lt;span class="s2"&gt;)
  -q            : Query for monthly credits which will include purchased credits
  -g &amp;lt;gid&amp;gt;      : Group ID for usage summary
  -m &amp;lt;months&amp;gt;   : Months of history (default: &lt;/span&gt;&lt;span class="nv"&gt;$MONTHS&lt;/span&gt;&lt;span class="s2"&gt;)
  -r            : Only print the remaining minutes.
"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this we're going to set some global flags that the options will manipulate, and; we can set our global defaults. At the top of our script, lets set these values and enable strict mode while we're at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;MONTH_CREDITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;400
&lt;span class="nv"&gt;MONTHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;span class="nv"&gt;GID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"9999"&lt;/span&gt;

&lt;span class="nv"&gt;IS_QUERY_CREDITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;IS_REMAINING_ONLY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While we're at it we can create some helper functions to ensure the user has passed correct integer values, and that the required &lt;code&gt;glab&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; commands are made available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;require_cmd&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt; is required for this script to function. See &lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

require_int&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[0-9]+&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt; must be a positive integer number"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    show_help
    &lt;span class="nb"&gt;exit &lt;/span&gt;2
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This now enables us the power to put it all together and add the options parsing and validation behaviour before calling our logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;getopts &lt;/span&gt;hc:qg:m:r OPTION&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  case&lt;/span&gt; &lt;span class="nv"&gt;$OPTION&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;h&lt;span class="p"&gt;)&lt;/span&gt;  show_help
        &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;
    c&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;MONTH_CREDITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;require_int &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPTARG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"credits"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    q&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;IS_QUERY_CREDITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    g&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;GID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;require_int &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPTARG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"group id"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    m&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;MONTHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;require_int &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPTARG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"months"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    r&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;IS_REMAINING_ONLY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="se"&gt;\?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; show_help &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
        &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;OPTIND &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;

&lt;span class="c"&gt;# I'm keeping this here so -h works&lt;/span&gt;
require_cmd glab &lt;span class="s2"&gt;"https://docs.gitlab.com/cli/"&lt;/span&gt;
require_cmd jq &lt;span class="s2"&gt;"https://jqlang.org/"&lt;/span&gt;

&lt;span class="nv"&gt;month_credits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_credits &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MONTH_CREDITS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IS_QUERY_CREDITS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; IS_REMAINING_ONLY &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;show_month_remaining &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;show_month_elapsed
  show_usage_history &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$month_credits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MONTHS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parting Thoughts
&lt;/h2&gt;

&lt;p&gt;Over the years I've had the privilige to teach many newcomers, help others solve problems and am often asked "how do you know all this?". My will always reply with a sentiment that is shared with artists and musicians; we aren't born with it, what you don't see is the countless hours of us practicing, frustrated, excited, building up callouses late at night when nobody is watching. We developers in my eyes are artists of a different canvas, and we should hold pride in the work we do. When you have this mindset, your quality of output improves, you want to share it with others because of all the hard work you put into it.&lt;/p&gt;

&lt;p&gt;Today while developers live in a world with Artificial Intelligence (AI), it can become easy for us to become complacent and rely on the AI which can rob us from skills that were so second nature to us that we wouldn't think about it when doing it. If we don't flex that muscle, it becomes weak to the point it can become painful to use. I encourage you to regularly flex those muscles by practicing with tools like these on your own projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  License Note
&lt;/h2&gt;

&lt;p&gt;This article is licensed under the &lt;a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener noreferrer"&gt;Creative Commons Attribution 4.0 International license (CC BY 4.0)&lt;/a&gt; You are free to share, copy, and adapt this material, provided you give appropriate credit and link back to this original post. Any companion scripts featured in this article are distributed under the &lt;a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noopener noreferrer"&gt;Apache License 2.0&lt;/a&gt; and made available as &lt;a href="https://gitlab.com/users/brett.ryan/snippets" rel="noopener noreferrer"&gt;snippets on my GitLab Profile&lt;/a&gt;.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;ClickOps emerged organically within the DevOps community circa 2017–2019 as a tongue-in-cheek contrast to the disciplined Infrastructure-as-Code (IaC) which gained wide-spread adoption.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>cli</category>
      <category>devops</category>
      <category>finops</category>
      <category>gitlab</category>
    </item>
    <item>
      <title>Keeping Dependencies Current</title>
      <dc:creator>Brett Ryan</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:50:41 +0000</pubDate>
      <link>https://dev.to/brettryan/keeping-dependencies-current-30la</link>
      <guid>https://dev.to/brettryan/keeping-dependencies-current-30la</guid>
      <description>&lt;p&gt;Working on a project of almost any size will almost always require the use of external dependencies that can often put us in a position of alert fatigue where we might have to chase our projects for CVE's that have been announced where it can feel like constantly plugging a dam. Using a technique to automate this chore which can be reviewed at a later date is extremely helpful.&lt;/p&gt;

&lt;p&gt;There are widely available tools in the ecosystem, however; I'm presenting this as a fun exercise to demonstrate how we can get in touch with the machinery that we can implement to do this.&lt;/p&gt;

&lt;p&gt;What we will do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://start.spring.io" rel="noopener noreferrer"&gt;start.spring.io&lt;/a&gt; to create a simple project.&lt;/li&gt;
&lt;li&gt;Configure pom project with specific dependency rules.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;dependencies.sh&lt;/code&gt; script.&lt;/li&gt;
&lt;li&gt;Add GitLab CI job to perform automatic dependency checks on a &lt;code&gt;chore/version-updates&lt;/code&gt; branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Full Script
&lt;/h4&gt;

&lt;p&gt;You can obtain a copy of the &lt;a href="https://gitlab.com/-/snippets/5986856" rel="noopener noreferrer"&gt;full script&lt;/a&gt; which has been published as a public GitLab snippet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Create Your Project
&lt;/h2&gt;

&lt;p&gt;Using the spring starter at &lt;a href="https://start.spring.io" rel="noopener noreferrer"&gt;start.spring.io&lt;/a&gt;, create a maven based project. As we're using the &lt;a href="https://www.mojohaus.org/versions-maven-plugin/" rel="noopener noreferrer"&gt;versions-maven-plugin&lt;/a&gt; we will require this to be a maven project.&lt;/p&gt;

&lt;p&gt;You can use &lt;a href="https://start.spring.io/#!type=maven-project&amp;amp;language=java&amp;amp;platformVersion=4.0.6&amp;amp;packaging=jar&amp;amp;configurationFileFormat=properties&amp;amp;jvmVersion=26&amp;amp;groupId=com.example&amp;amp;artifactId=demo&amp;amp;packageName=com.example.demo&amp;amp;dependencies=" rel="noopener noreferrer"&gt;this template&lt;/a&gt; which is a bookmark of a spring 4.0.6 project using maven and JDK 26.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Project to Ignore pre-releases
&lt;/h2&gt;

&lt;p&gt;I've found that in maven central, some projects will release release candidates, milestones, alpha and other releases that I do not want to have my dependency management script to try an upgrade to. My original approach to solve this was to set the &lt;code&gt;maven.version.ignore&lt;/code&gt; maven property to ignore these&lt;br&gt;
patterns, like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;maven.version.ignore&amp;gt;&lt;/span&gt;(?i).*[\-\.](m|rc|dev|alpha|beta)[\-\.]?[0-9]*&lt;span class="nt"&gt;&amp;lt;/maven.version.ignore&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;processDependencyManagementTransitive&amp;gt;&lt;/span&gt;${processDependencyManagementTransitive}&lt;span class="nt"&gt;&amp;lt;/processDependencyManagementTransitive&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well, however; if you want to have rules where you may want to be using pre-releases or milestones from specific vendors, i.e. you may be testing the newest spring milestones, you will need to extend this by adding a &lt;code&gt;ruleSet&lt;/code&gt; to the &lt;code&gt;build/plugins&lt;/code&gt; section of your pom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.codehaus.mojo&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;versions-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ruleSet&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;rules&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;rule&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;ignoreVersions&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;ignoreVersion&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;regex&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;(?i).*&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"m|rc|dev|alpha|beta"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;\-\.&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;[\-\.]?[0-9]*&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/ignoreVersion&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/ignoreVersions&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/rule&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;rule&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-parent&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/rule&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;rule&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.modulith&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-modulith-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/rule&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/rules&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ruleSet&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;processDependencyManagementTransitive&amp;gt;&lt;/span&gt;${processDependencyManagementTransitive}&lt;span class="nt"&gt;&amp;lt;/processDependencyManagementTransitive&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that I've also set the property &lt;code&gt;processDependencyManagementTransitive&lt;/code&gt;, keep this in your &lt;code&gt;properties&lt;/code&gt; section as our script will set this later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency Update Script
&lt;/h2&gt;

&lt;p&gt;We will now create a script that will perform the dependency update check and optionally perform a &lt;code&gt;git commit&lt;/code&gt; if updates were found.&lt;/p&gt;

&lt;p&gt;There are two sets of maven goals we could use&lt;/p&gt;

&lt;p&gt;To show only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;versions:display-dependency-updates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;versions:display-plugin-updates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;versions:display-property-updates&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or, to perform the updates my modifying the pom.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;versions:update-parent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;versions:update-properties&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;versions:use-latest-versions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Refer to the &lt;a href="https://www.mojohaus.org/versions/versions-maven-plugin/index.html" rel="noopener noreferrer"&gt;versions-maven-plugin&lt;/a&gt; documentation for more on what these goals do.&lt;/p&gt;

&lt;p&gt;We will write a script that gives us the following capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usage: dependencies.sh [-cCsStu]

-c  : Perform git commit opening editor for review
-C  : Perform git commit
-s  : Don't perform snapshot updates
-S  : Force update snapshot updates
-t  : Process transitive dependencies
-u  : Perform update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this script, we will begin with the setup and a &lt;code&gt;getopts&lt;/code&gt; block that takes these command line arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nv"&gt;goals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"
  versions:display-dependency-updates
  versions:display-plugin-updates
  versions:display-property-updates
"&lt;/span&gt;

&lt;span class="nv"&gt;snaps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;margs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;getopts &lt;/span&gt;cChsStu OPTION&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  case&lt;/span&gt; &lt;span class="nv"&gt;$OPTION&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;h&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"
Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; [-cCsStu]

-c  : Perform git commit opening editor for review
-C  : Perform git commit
-s  : Don't perform snapshot updates
-S  : Force update snapshot updates
-t  : Process transitive dependencies
-u  : Perform update
"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
        &lt;span class="nb"&gt;exit &lt;/span&gt;1
        &lt;span class="p"&gt;;;&lt;/span&gt;
    c&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;commit_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;edit &lt;span class="p"&gt;;;&lt;/span&gt;
    C&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;commit_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;perform &lt;span class="p"&gt;;;&lt;/span&gt;
    s&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;snaps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-nsu"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    S&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;snaps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-U"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    t&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;margs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;margs&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -DprocessDependencyManagementTransitive=true"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    u&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nv"&gt;goals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"
          versions:update-parent
          versions:update-properties
          versions:use-latest-versions
        "&lt;/span&gt;
        &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;OPTIND &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the user to either view (default) or update (-u). When viewing the caller can test for transitive dependency updates with &lt;code&gt;-t&lt;/code&gt;. We can also commit changes automatically with &lt;code&gt;-c&lt;/code&gt; to open an editor or have &lt;code&gt;-C&lt;/code&gt; perform the commit.&lt;/p&gt;

&lt;p&gt;In order to capture the dependencies that were updated, we will &lt;code&gt;tee&lt;/code&gt; the output from maven (using &lt;code&gt;./mvnw&lt;/code&gt; here) to allow us to parse the content when complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;mvn_log&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Couldn't create temp file"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -f "$mvn_log"'&lt;/span&gt; EXIT

&lt;span class="nb"&gt;echo&lt;/span&gt; ./mvnw &lt;span class="nv"&gt;$snaps&lt;/span&gt; &lt;span class="nv"&gt;$margs&lt;/span&gt; &lt;span class="nv"&gt;$goals&lt;/span&gt;
./mvnw &lt;span class="nv"&gt;$snaps&lt;/span&gt; &lt;span class="nv"&gt;$margs&lt;/span&gt; &lt;span class="nv"&gt;$goals&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nv"&gt;$mvn_log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we will parse the output file to list off all our changes. A regular expression here formats the content into the following form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;- &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;DEPENDENCY&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;: &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;FROM&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt; -&amp;gt; &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;TO&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This format will strip out property managed versions such as &lt;code&gt;${spring-modulith.version}&lt;/code&gt; and replace it with &lt;code&gt;spring-modulith&lt;/code&gt; to make our git commit more readable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;changes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Updated "&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mvn_log&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;|&lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/^.* Updated (\$\{)?([^.}]*)(.version)?\}? from (.*) to (.*)$/- \2: \4 -&amp;gt; \5/'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Regular Expression Deep Dive
&lt;/h4&gt;

&lt;p&gt;We're creating an expression that looks for lines that have the following core pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;^.* Updated&lt;/code&gt;: Looks from the start of line until we reach the word &lt;code&gt;Updated&lt;/code&gt; with a leading and trailing space.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(\$\{)?&lt;/code&gt;: May contain the sequence &lt;code&gt;${&lt;/code&gt; which we want to ignore so must remember to skip this capture group.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;([^.}])&lt;/code&gt;: Capture all characters that are not &lt;code&gt;'.'&lt;/code&gt; or &lt;code&gt;'}'&lt;/code&gt;, there is a slight issue with this in that if you use a property that doesn't end in &lt;code&gt;.version&lt;/code&gt; but does have a dot &lt;code&gt;'.'&lt;/code&gt; in it's name, it might not be processed correctly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(.version)?\}?&lt;/code&gt;: skip the optional phrase &lt;code&gt;.version&lt;/code&gt; and the optional &lt;code&gt;}&lt;/code&gt; character.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;from (.*) to (.*)$&lt;/code&gt; capture the from and to versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all these sequences match the line, it will be replaced with the capture groups we are interested in &lt;code&gt;- \2: \4 -&amp;gt; \5&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can now test for this to see if there were any changes detected, if there are, we can then perform the determined git commit option chosen by the caller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$changes&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No changes detected to commit."&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;commit_msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"[chore] Dependency and plugin updates

&lt;/span&gt;&lt;span class="nv"&gt;$changes&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commit_msg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .git/COMMIT_EDITMSG
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commit_mode&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; --- Changes staged for commit ---"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;--- Committing Changes ---"&lt;/span&gt;
    git add pom.xml &lt;span class="k"&gt;**&lt;/span&gt;/pom.xml 2&amp;gt;/dev/null
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commit_mode&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"edit"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commit_msg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commit_msg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup GitLab Automation
&lt;/h2&gt;

&lt;p&gt;This could now be setup on an automated &lt;code&gt;chore/version-updates&lt;/code&gt; branch to be performed once a week or on a schedule of your choosing.&lt;/p&gt;

&lt;p&gt;Configure your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; with blocks similar to the following, your configuration may look different, however; in my case I use GraalVM CE edition for my use-cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;.maven-base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/graalvm/jdk-community:25&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maven-shared-cache"&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.m2/repository&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.sonar/cache&lt;/span&gt;

&lt;span class="na"&gt;version_updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.maven-base&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;microdnf install -y git&lt;/span&gt;

    &lt;span class="s"&gt;git config --global user.name "GitLab CI Bot"&lt;/span&gt;
    &lt;span class="s"&gt;git config --global user.email "brett.ryan+gitlab-bot@gmail.com"&lt;/span&gt;
    &lt;span class="s"&gt;git remote set-url origin https://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git&lt;/span&gt;

    &lt;span class="s"&gt;./dependencies.sh -u -C&lt;/span&gt;
    &lt;span class="s"&gt;if [[ "$(git rev-list --count @{u}..HEAD)" != "0" ]]; then&lt;/span&gt;
      &lt;span class="s"&gt;git pull origin ${CI_COMMIT_REF_NAME} --rebase&lt;/span&gt;
      &lt;span class="s"&gt;git push origin HEAD:${CI_COMMIT_REF_NAME}&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;

  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_PIPELINE_SOURCE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"schedule"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"chore/version-updates"'&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;h4&gt;
  
  
  GITLAB_TOKEN Best Practices
&lt;/h4&gt;

&lt;p&gt;Ensure that you create a masked Group Access Token or Project Access Token for &lt;code&gt;GITLAB_TOKEN&lt;/code&gt;. This token will need to have &lt;code&gt;write_repository&lt;/code&gt; access.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this script we take advantage of the script using the &lt;code&gt;-C&lt;/code&gt; option to perform the automated commit, which; can then be detected with &lt;code&gt;git rev-list --count @{u}..HEAD&lt;/code&gt; to detect if changes were made. If they were, then we can then push the commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;There are several ways you could progress this by having full regression tests performed, notifications to slack or teams to notify the development team when the branch has been updated.&lt;/p&gt;

&lt;p&gt;You could now integrate this into a release cycle where a QA manager performs validation and owns the &lt;code&gt;chore&lt;/code&gt; branch, and; what should be done in the negative scenario where there may be versions that are not desired? Would you make it a practice to include them into the &lt;code&gt;ruleSet&lt;/code&gt;? This could even be automated based on build failures.&lt;/p&gt;

</description>
      <category>java</category>
      <category>maven</category>
      <category>gitlabci</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
