<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://freshbrewed.science/feed.xml" rel="self" type="application/atom+xml" /><link href="https://freshbrewed.science/" rel="alternate" type="text/html" /><updated>2026-05-06T20:26:33+00:00</updated><id>https://freshbrewed.science/feed.xml</id><title type="html">Fresh/Brewed</title><subtitle>Tales from Cloudy McCloudface.
</subtitle><entry><title type="html">Code clean tools: Fallow and skills</title><link href="https://freshbrewed.science/2026/05/07/codeclean.html" rel="alternate" type="text/html" title="Code clean tools: Fallow and skills" /><published>2026-05-07T01:00:01+00:00</published><updated>2026-05-07T01:00:01+00:00</updated><id>https://freshbrewed.science/2026/05/07/codeclean</id><content type="html" xml:base="https://freshbrewed.science/2026/05/07/codeclean.html"><![CDATA[<p>I recently came across <a href="https://www.youtube.com/watch?v=XLtuSy1opW4">this YT review of Fallow</a> which is <a href="https://docs.fallow.tools/">easy to run</a> and similar to <a href="https://github.com/peteromallet/desloppify">desloppify</a> is meant to help cleanup vibe code (but also just code in general).  In this post I’ll set it up and explore some of its features and touch on some of the paid ones as well.</p>

<p>Since it really is just focused on JavaScript and Typescript, I then follow that up with a variety of code cleaning skills such as clean code skills from <a href="https://github.com/sammasak/claude-code-skills">these claude-code-skills</a>.</p>

<p>Let’s start with <a href="https://fallow.tools/">Fallow</a>.</p>

<h1 id="fallow">Fallow</h1>

<p>I have a project underway that might be able to use something like this.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) builder@DESKTOP-QADGF36:~/Workspaces/offenquelle$ ls
Dockerfile               helm-chart
HELM_SETUP.md            nanobanana-output
INSTALL.md               offenquelle_landing_page.egg-info
README.md                openapi.yaml
SYSTEM.md                pyproject.toml
app                      pytest.ini
app.db                   tests
build                    venv
example_invvokation.txt
</code></pre></div></div>

<p>I was legitimately surprised to see that I had no issues at the start</p>

<p><a href="/content/images/2026/05/fallow-01.png"><img src="/content/images/2026/05/fallow-01.png" alt="/content/images/2026/05/fallow-01.png" /></a></p>

<p>This could be because the bulk of this project is Python and <a href="https://docs.fallow.tools/analysis/file-types">Fallow is really more about HTML and JS frameworks</a></p>

<p>Let’s pivot over to my <a href="https://wildtrack.in">Wildtrack</a> repo which I know needs some cleaning as this was the one we did “desloppify” on before</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ npx fallow
   0.007073804s  WARN node_modules directory not found. Run `npm install` / `pnpm install` first for accurate results.
  note: git churn analysis took 0.6s (cached for next run at same HEAD)

■ Metrics: dead files 6.0% (6 of 100) · dead exports 6.4% (6 of 94) · MI 91.8 (good) · 1 churn hotspot
  100 files analyzed
  17 entry points detected (16 plugin, 1 default index)
  1 refactoring target — start with server.js (untested risk)

── Dead Code ──────────────────────────────────────

── Unused Code ─────────────────────────────────────

● Unused files (6)
  client/src/App.css
  client/src/test/setup.ts
  server/auth.js
  server/database.js
  server/email.js
  server/server.js
  Files not reachable from any entry point — https://docs.fallow.tools/explanations/dead-code#unused-files
  To suppress: // fallow-ignore-next-line unused-files

● Unused exports (2)
  client/src/components/inventory/GearIcon.tsx
    :3 getGearIcon
  client/src/utils/imageUtils.ts
    :3 createImage
  Exported symbols with no known consumers — https://docs.fallow.tools/explanations/dead-code#unused-exports
  (2 more in files already reported as unused)

● Unused type exports (2)
  client/src/hooks/useTrailChat.ts
    :4 Participant
  client/src/hooks/useUserSearch.ts
    :3 UserSearchResult
  Type exports with no known consumers — https://docs.fallow.tools/explanations/dead-code#unused-types

✗ 6 files · 2 exports · 2 types (0.12s)

── Duplication ────────────────────────────────────

● Duplicates (46 clone groups)

     35 lines  3 instances
    client/src/hooks/useTripEditor.ts:107-141
    client/src/hooks/useTripLogistics.ts:38-61
    client/src/hooks/useTripPlanning.ts:94-105

     34 lines  2 instances
    client/src/components/shared/MapWidget.tsx:38-71
    client/src/components/trip-sections/TripMap.tsx:31-63

     27 lines  2 instances
    client/src/components/shared/MapWidget.tsx:177-189
    client/src/hooks/useMapInstance.ts:24-50

     22 lines  2 instances
    client/src/pages/admin/Admin.tsx:37-58
    client/src/pages/general/Settings.tsx:103-116

     19 lines  2 instances
    server/server.js:737-755
    server/server.js:758-773

     17 lines  2 instances
    client/src/components/trip-editor/TripScheduleEditor.tsx:88-103
    client/src/components/trip-sections/TripLogistics.tsx:132-148

     15 lines  2 instances
    client/src/components/trip-editor/TripTeamManager.tsx:65-79
    client/src/pages/trips/TripPlanning.tsx:111-112

     14 lines  3 instances
    client/src/components/shared/MapWidget.tsx:49-62
    client/src/components/trip-sections/TripMap.tsx:42-55
    client/src/utils/mapStyles.ts:6-19

     14 lines  2 instances
    client/src/components/trip-view/TripParticipants.tsx:79-92
    client/src/pages/trips/TripPlanning.tsx:111-112

     14 lines  2 instances
    server/server.js:577-588
    server/server.js:1299-1312

  ... and 36 more clone groups
  Identical code blocks detected via suffix-array analysis — https://docs.fallow.tools/explanations/duplication#clone-groups

● Clone families (7 with multiple groups)

  3 groups, 44 lines across client/src/components/shared/MapWidget.tsx, client/src/hooks/useMapInstance.ts
    → Extract shared function (27 lines) from MapWidget.tsx, useMapInstance.ts
    → Extract shared function (10 lines) from MapWidget.tsx, useMapInstance.ts
    → Extract shared function (7 lines) from MapWidget.tsx, useMapInstance.ts

  2 groups, 14 lines across client/src/components/trip-sections/TripMap.tsx, client/src/hooks/useMapInstance.ts
    → Extract shared function (9 lines) from TripMap.tsx, useMapInstance.ts
    → Extract shared function (5 lines) from TripMap.tsx, useMapInstance.ts

  2 groups, 25 lines across client/src/components/trip-view/TripParticipants.tsx, client/src/pages/trips/TripPlanning.tsx
    → Extract shared function (11 lines) from TripParticipants.tsx, TripPlanning.tsx
    → Extract shared function (14 lines) from TripParticipants.tsx, TripPlanning.tsx

  2 groups, 43 lines across client/src/hooks/useTripEditor.ts, client/src/hooks/useTripLogistics.ts, client/src/hooks/useTripPlanning.ts
    → Extract shared function (35 lines) from useTripEditor.ts, useTripLogistics.ts, useTripPlanning.ts
    → Extract shared function (8 lines) from useTripEditor.ts, useTripLogistics.ts, useTripPlanning.ts

  2 groups, 14 lines across client/src/hooks/useTripEditor.ts, client/src/hooks/useTripPlanning.ts
    → Extract shared function (9 lines) from useTripEditor.ts, useTripPlanning.ts
    → Extract shared function (5 lines) from useTripEditor.ts, useTripPlanning.ts

  2 groups, 14 lines across client/src/pages/gear/Inventory.test.tsx, client/src/pages/general/Settings.test.tsx
    → Extract shared function (7 lines) from Inventory.test.tsx, Settings.test.tsx
    → Extract shared function (7 lines) from Inventory.test.tsx, Settings.test.tsx

  20 groups, 178 lines across server/server.js
    → Extract 20 shared clone groups (178 lines) from server.js into server

  Groups of related clones across the same files — https://docs.fallow.tools/explanations/duplication#clone-families

✗ 870 lines (9.4%) duplicated across 29 files (0.02s)

── Complexity ─────────────────────────────────────

■ Metrics: 9,497 LOC · dead files 6.0% · dead exports 6.4% · avg cyclomatic 2.3 · p90 cyclomatic 4 · maintainability 91.8 (good) · 1 churn hotspot

  Function size: 74% low · 14% medium · 6% high · 6% very high  (1-15 / 16-30 / 31-60 / &gt;60 LOC)

● Large functions (10 shown, 53 total)
  server/database.js
    :26 initDb  300 lines
  client/src/components/shared/MapWidget.tsx
    :15 MapWidget  214 lines
  client/src/components/trip-sections/TripFiles.tsx
    :9 TripFiles  181 lines
  client/src/components/trip-sections/TripLinks.tsx
    :9 TripLinks  165 lines
  client/src/hooks/useTripEditor.ts
    :4 useTripEditor  164 lines
  client/src/components/trip-sections/TripMap.tsx
    :11 TripMap  139 lines
  client/src/hooks/useAdminData.ts
    :17 useAdminData  138 lines
  client/src/pages/general/Settings.tsx
    :11 Settings  138 lines
  client/src/pages/trips/TripPlanning.tsx
    :7 TripPlanning  138 lines
  client/src/components/trip-sections/TripLogistics.tsx
    :17 TripLogistics  132 lines
  Functions exceeding 60 lines of code (very high risk): https://docs.fallow.tools/explanations/health#unit-size
  use --top 53 to see all

● High complexity functions (50)
  client/src/pages/general/Settings.tsx
    :11 Settings CRITICAL
          28 cyclomatic   16 cognitive  138 lines
         197.3 CRAP
  client/src/components/auth/AuthCard.tsx
    :27 AuthCard HIGH
          23 cyclomatic   27 cognitive   99 lines
  client/src/hooks/useAuthForm.ts
    :45 handleSubmit HIGH
          16 cyclomatic   19 cognitive   56 lines
          71.3 CRAP
  client/src/components/trip-view/TripParticipants.tsx
    :36 &lt;arrow&gt; HIGH
          16 cyclomatic   13 cognitive   28 lines
          71.3 CRAP
  server/server.js
    :871 &lt;arrow&gt; CRITICAL
          15 cyclomatic   14 cognitive   87 lines
         240.0 CRAP
  client/src/components/trip-sections/TripLogistics.tsx
    :17 TripLogistics
          14 cyclomatic   17 cognitive  132 lines
  server/server.js
    :693 &lt;arrow&gt; CRITICAL
          14 cyclomatic   13 cognitive   43 lines
         210.0 CRAP
  client/src/components/inventory/GearIcon.tsx
    :3 getGearIcon HIGH
          14 cyclomatic    1 cognitive   18 lines
          56.3 CRAP
  client/src/pages/trips/TripPlanning.tsx
    :7 TripPlanning HIGH
          14 cyclomatic   12 cognitive  138 lines
          56.3 CRAP
  client/src/components/chat/ChatWindow.tsx
    :67 &lt;arrow&gt; HIGH
          14 cyclomatic    8 cognitive   21 lines
          56.3 CRAP
  client/src/components/trip-sections/TripChat.tsx
    :42 &lt;arrow&gt; HIGH
          14 cyclomatic    8 cognitive   14 lines
          56.3 CRAP
  client/src/pages/trips/TripDetails.tsx
    :20 TripDetails
          13 cyclomatic    4 cognitive  118 lines
          49.5 CRAP
  client/src/components/trip-sections/TripMap.tsx
    :26 initMapAndRoute
          12 cyclomatic   19 cognitive   83 lines
          43.1 CRAP
  server/server.js
    :662 &lt;arrow&gt; CRITICAL
          11 cyclomatic   10 cognitive   30 lines
         132.0 CRAP
  client/src/components/ui/Sidebar.tsx
    :10 Sidebar
          11 cyclomatic    6 cognitive   89 lines
          37.1 CRAP
  client/src/hooks/useLeaveTrip.ts
    :23 handleLeaveConfirm
          11 cyclomatic   13 cognitive   36 lines
          37.1 CRAP
  client/src/hooks/useMealPlan.ts
    :22 &lt;arrow&gt;
          11 cyclomatic   13 cognitive   52 lines
          37.1 CRAP
  client/src/hooks/useTrailChat.ts
    :83 initChat
          10 cyclomatic   17 cognitive   32 lines
          31.6 CRAP
  server/server.js
    :283 &lt;arrow&gt; CRITICAL
          10 cyclomatic    8 cognitive   41 lines
         110.0 CRAP
  client/src/components/trip-sections/TripNotes.tsx
    :11 TripNotes
           9 cyclomatic   19 cognitive  100 lines
  server/server.js
    :1262 &lt;arrow&gt; HIGH
           9 cyclomatic   10 cognitive   34 lines
          90.0 CRAP
  server/database.js
    :153 &lt;arrow&gt; HIGH
           8 cyclomatic    7 cognitive   34 lines
          72.0 CRAP
  server/server.js
    :634 &lt;arrow&gt; HIGH
           7 cyclomatic    6 cognitive   27 lines
          56.0 CRAP
    :737 &lt;arrow&gt; HIGH
           7 cyclomatic    7 cognitive   20 lines
          56.0 CRAP
    :758 &lt;arrow&gt; HIGH
           7 cyclomatic    6 cognitive   27 lines
          56.0 CRAP
    :1226 &lt;arrow&gt; HIGH
           7 cyclomatic    4 cognitive   27 lines
          56.0 CRAP
    :157 &lt;anonymous&gt;
           6 cyclomatic    5 cognitive   16 lines
          42.0 CRAP
    :1093 &lt;arrow&gt;
           6 cyclomatic    4 cognitive   18 lines
          42.0 CRAP
    :1159 &lt;arrow&gt;
           6 cyclomatic    4 cognitive   12 lines
          42.0 CRAP
    :1210 &lt;arrow&gt;
           6 cyclomatic    4 cognitive    9 lines
          42.0 CRAP
    :1326 &lt;arrow&gt;
           6 cyclomatic    7 cognitive   10 lines
          42.0 CRAP
    :1418 &lt;arrow&gt;
           6 cyclomatic    7 cognitive   11 lines
          42.0 CRAP
  client/src/components/admin/AdminBackups.tsx
    :14 AdminBackups
           6 cyclomatic    5 cognitive   95 lines
          42.0 CRAP
  client/src/pages/admin/Admin.tsx
    :7 Admin
           6 cyclomatic    4 cognitive   62 lines
          42.0 CRAP
  server/auth.js
    :50 &lt;arrow&gt;
           5 cyclomatic    4 cognitive   21 lines
          30.0 CRAP
  server/server.js
    :98 &lt;anonymous&gt;
           5 cyclomatic    4 cognitive   15 lines
          30.0 CRAP
    :822 &lt;anonymous&gt;
           5 cyclomatic    6 cognitive   37 lines
          30.0 CRAP
    :807 &lt;arrow&gt;
           5 cyclomatic    4 cognitive   55 lines
          30.0 CRAP
    :863 &lt;arrow&gt;
           5 cyclomatic    4 cognitive   96 lines
          30.0 CRAP
    :965 &lt;arrow&gt;
           5 cyclomatic    3 cognitive   10 lines
          30.0 CRAP
    :983 &lt;arrow&gt;
           5 cyclomatic    3 cognitive   10 lines
          30.0 CRAP
    :1018 &lt;arrow&gt;
           5 cyclomatic    3 cognitive   10 lines
          30.0 CRAP
    :1034 &lt;arrow&gt;
           5 cyclomatic    3 cognitive   10 lines
          30.0 CRAP
    :1468 &lt;arrow&gt;
           5 cyclomatic    4 cognitive   17 lines
          30.0 CRAP
    :1454 &lt;arrow&gt;
           5 cyclomatic    4 cognitive   32 lines
          30.0 CRAP
  client/src/components/admin/AdminUsers.tsx
    :38 &lt;arrow&gt;
           5 cyclomatic    4 cognitive   26 lines
          30.0 CRAP
  client/src/hooks/useAdminData.ts
    :37 fetchUsers
           5 cyclomatic    5 cognitive   18 lines
          30.0 CRAP
    :61 handleDeleteUser
           5 cyclomatic    4 cognitive   18 lines
          30.0 CRAP
    :95 handleDeleteBackup
           5 cyclomatic    4 cognitive   12 lines
          30.0 CRAP
    :108 handleRestoreBackup
           5 cyclomatic    4 cognitive   27 lines
          30.0 CRAP
  Functions exceeding cyclomatic, cognitive, or CRAP thresholds (https://docs.fallow.tools/explanations/health#complexity-metrics)
  To suppress: // fallow-ignore-next-line complexity

● File health scores (92 files)

   64.3    server/server.js
           1519 LOC    0 fan-in    3 fan-out  100% dead  0.34 density  240.0 risk

   67.9    server/auth.js
             75 LOC    1 fan-in    1 fan-out  100% dead  0.31 density  30.0 risk

   74.0    server/database.js
            328 LOC    2 fan-in    0 fan-out  100% dead  0.20 density  72.0 risk

   76.4    server/email.js
             33 LOC    1 fan-in    0 fan-out  100% dead  0.18 density  12.0 risk

   78.2    client/src/test/setup.ts
             16 LOC    0 fan-in    0 fan-out  100% dead  0.19 density  2.0 risk

   81.0    client/src/components/inventory/GearIcon.tsx
             30 LOC    1 fan-in    0 fan-out   50% dead  0.50 density  56.3 risk

   81.8    client/src/pages/general/Settings.tsx
            149 LOC    2 fan-in    7 fan-out    0% dead  0.33 density  197.3 risk

   83.3    client/src/pages/trips/TripDetails.tsx
            138 LOC    2 fan-in   16 fan-out    0% dead  0.18 density  49.5 risk

   84.0    client/src/pages/trips/TripPlanning.tsx
            145 LOC    2 fan-in    4 fan-out    0% dead  0.32 density  56.3 risk

   84.7    client/src/utils/imageUtils.ts
             42 LOC    1 fan-in    0 fan-out   50% dead  0.21 density  3.0 risk

  ... and 82 more files (--format json for full list)

  Composite file quality scores based on complexity, coupling, and dead code. Risk: low &lt;15, moderate 15-30, high &gt;=30. CRAP estimated from export references (85% direct, 40% indirect, 0% untested). Use --coverage for exact scores. https://docs.fallow.tools/explanations/health#file-health-scores

● Hotspots (5 files, since 6 months)

  100.0 ▼  server/server.js
          25 commits   1688 churn  0.34 density   0 fan-in  ▼ cooling

   44.6 ─  server/database.js
          19 commits    369 churn  0.20 density   2 fan-in  ─ stable

   16.7 ▼  client/src/App.tsx
          13 commits    107 churn  0.11 density   1 fan-in  ▼ cooling

   14.6 ─  server/auth.js
           4 commits     78 churn  0.31 density   1 fan-in  ─ stable

    5.7 ─  client/src/context/AuthContext.tsx
           4 commits     94 churn  0.12 density   7 fan-in  ─ stable

  87 files excluded (&lt; 3 commits)

  Files with high churn and high complexity — https://docs.fallow.tools/explanations/health#hotspot-metrics

● Refactoring targets (1)
  1 high
    score = quick-win ROI (higher = better) · pri = absolute priority

   19.7  pri:59.0    server/server.js
         untested risk · effort:high · confidence:high  25 complex functions lack test coverage path, add tests before modifying

  Prioritized refactoring recommendations based on complexity, churn, and coupling signals — https://docs.fallow.tools/explanations/health#refactoring-targets

✗ 50 above threshold · 833 analyzed · maintainability 91.8 (good) (0.61s)

Failed: dead-code (12 issues), dupes (46 clone groups), health (50 above threshold) — start with server.js
</code></pre></div></div>

<p>Let’s say we wanted to correct these.</p>

<p>We could use the <code class="language-plaintext highlighter-rouge">dry-run</code> first in the part of the repo with JS code</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp/client$ npm install

up to date, audited 406 packages in 3s

158 packages are looking for funding
  run `npm fund` for details

9 vulnerabilities (3 moderate, 6 high)

To address all issues, run:
  npm audit fix

Run `npm audit` for details.
(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp/client$ npx fallow fix --dry-run
Would remove export from src/utils/imageUtils.ts:3 `createImage`
Would remove export from src/components/inventory/GearIcon.tsx:3 `getGearIcon`
Would remove `lucide-react` from dependencies in /home/builder/Workspaces/wildernessapp/client/package.json
Would remove `@tailwindcss/typography` from devDependencies in /home/builder/Workspaces/wildernessapp/client/package.json
Would remove `autoprefixer` from devDependencies in /home/builder/Workspaces/wildernessapp/client/package.json
Dry run complete. No files were modified.
</code></pre></div></div>

<p>Let me do two things here - an <code class="language-plaintext highlighter-rouge">audit fix</code> for CVEs and <code class="language-plaintext highlighter-rouge">fallow fix</code> to cleanup code</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp/client$ npx fallow fix --dry-run
Would remove export from src/utils/imageUtils.ts:3 `createImage`
Would remove export from src/components/inventory/GearIcon.tsx:3 `getGearIcon`
Would remove `lucide-react` from dependencies in /home/builder/Workspaces/wildernessapp/client/package.json
Would remove `@tailwindcss/typography` from devDependencies in /home/builder/Workspaces/wildernessapp/client/package.json
Would remove `autoprefixer` from devDependencies in /home/builder/Workspaces/wildernessapp/client/package.json
Dry run complete. No files were modified.
(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp/client$ npm audit fix

changed 13 packages, and audited 405 packages in 6s

158 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp/client$ npx fallow fix
Fixed 5 issue(s).
</code></pre></div></div>

<p>This handles exports and dependencies, but not dupes and dead code.</p>

<p>Perhaps we can feed this into Gemini CLI to get it cleaned up?</p>

<video muted="" controls="">
    <source src="/content/images/2026/05/fallow-02.mp4" type="video/mp4" />
</video>

<p>As we see in the results, Gemini listened to Fallows “dead-code” responses, but wisely noted the test file is actually used by Vite config:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Cleanup Complete: Finalize dead code cleanup investigation.


✦ I have verified the dead code reported by fallow and performed the following cleanup:

  1. Removed Unused Files
   * src/App.css: This file contained default Vite boilerplate and was not imported or used anywhere in the project. I have deleted
     it.
   * src/test/setup.ts: Note: I have chosen NOT to remove this file. Although fallow flags it as unused, it is explicitly referenced
     in vite.config.ts as a test setup file:

   1     // vite.config.ts
   2     setupFiles: ['./src/test/setup.ts'],

  2. Removed Unused Exports
  I downgraded the following types from export interface to interface because they are only used internally within their respective
  files:
   * Participant in src/hooks/useTrailChat.ts
   * UserSearchResult in src/hooks/useUserSearch.ts

  Verification Results
  After the cleanup, npx fallow dead-code now only reports src/test/setup.ts as unused, confirming that the other dead code has been
  successfully addressed.

</code></pre></div></div>

<p>Here we can see some of the examples of cleanup that was done including removing unnecessary exports</p>

<p><a href="/content/images/2026/05/fallow-03.png"><img src="/content/images/2026/05/fallow-03.png" alt="/content/images/2026/05/fallow-03.png" /></a></p>

<p>Let’s just see how Gemini handles Duplications.</p>

<p>Here is a partial recording of that session:</p>

<video muted="" controls="">
    <source src="/content/images/2026/05/fallow-04.mp4" type="video/mp4" />
</video>

<p>Which wrapped up after a long while with quite a lot of updates</p>

<p><a href="/content/images/2026/05/fallow-07.png"><img src="/content/images/2026/05/fallow-07.png" alt="/content/images/2026/05/fallow-07.png" /></a></p>

<p>I didn’t test building and running, but i did stash these changes in a branch</p>

<p><a href="/content/images/2026/05/fallow-08.png"><img src="/content/images/2026/05/fallow-08.png" alt="/content/images/2026/05/fallow-08.png" /></a></p>

<h2 id="pi">Pi</h2>

<p>I was curious how well Pi might do.</p>

<p>I cloned the skills</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/.agents/skills$ git clone https://github.com/fallow-rs/fallow-skills.git fallow-skills
Cloning into 'fallow-skills'...
remote: Enumerating objects: 1419, done.
remote: Counting objects: 100% (86/86), done.
remote: Compressing objects: 100% (59/59), done.
remote: Total 1419 (delta 24), reused 75 (delta 15), pack-reused 1333 (from 1)
Receiving objects: 100% (1419/1419), 381.65 KiB | 3.15 MiB/s, done.
Resolving deltas: 100% (701/701), done.
</code></pre></div></div>

<p>I then found it nested the “fallow” folder, so I moved that up.</p>

<p>I found Opencode with Minimax Free worked pretty well</p>

<p><a href="/content/images/2026/05/fallow-09.png"><img src="/content/images/2026/05/fallow-09.png" alt="/content/images/2026/05/fallow-09.png" /></a></p>

<p>But many of the Ollama models seemed to get hung up on the skill</p>

<p><a href="/content/images/2026/05/fallow-10.png"><img src="/content/images/2026/05/fallow-10.png" alt="/content/images/2026/05/fallow-10.png" /></a></p>

<p>I tried Qwen3 14b which seemed to work</p>

<p><a href="/content/images/2026/05/fallow-11.png"><img src="/content/images/2026/05/fallow-11.png" alt="/content/images/2026/05/fallow-11.png" /></a></p>

<p>And IBM’s new Granite 4.1 which did not</p>

<p><a href="/content/images/2026/05/fallow-12.png"><img src="/content/images/2026/05/fallow-12.png" alt="/content/images/2026/05/fallow-12.png" /></a></p>

<h2 id="plans">Plans</h2>

<p>There is some static file analysis that can be run with a trial.</p>

<p>I wanted to first understand costs.  If we go to <a href="https://fallow.tools/">the Fallow site</a> and to <a href="https://fallow.tools/pricing">Pricing</a> we can see that for individuals it is free, but for teams (greater than 2 contributors) it’s US$20/mo/dev</p>

<p><a href="/content/images/2026/05/fallow-05.png"><img src="/content/images/2026/05/fallow-05.png" alt="/content/images/2026/05/fallow-05.png" /></a></p>

<p>I can do a trial, but free tier will eventually block me and my only choice at that point would be to pay $60/mo (because they require a minimum of 3 users)</p>

<p><a href="/content/images/2026/05/fallow-06.png"><img src="/content/images/2026/05/fallow-06.png" alt="/content/images/2026/05/fallow-06.png" /></a></p>

<p>I find this rather odd as it would seem there should be some kind of full featured option for just a solo schmo like myself.</p>

<h1 id="skills-clean-code">Skills: Clean Code</h1>

<p>By way of Lobehub, I found <a href="https://github.com/sammasak/claude-code-skills">these claude-code-skills</a> from <a href="https://github.com/sammasak/">Sammasak</a>.</p>

<p>Let’s look at <a href="https://github.com/sammasak/claude-code-skills/blob/main/skills/clean-code-principles/SKILL.md">their Claude Code Skills - Clean Code Principals</a></p>

<p>I’ll clone it down</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/sammasak/claude-code-skills.git
Cloning into 'claude-code-skills'...
remote: Enumerating objects: 1018, done.
remote: Counting objects: 100% (376/376), done.
remote: Compressing objects: 100% (229/229), done.
remote: Total 1018 (delta 190), reused 289 (delta 114), pack-reused 642 (from 1)
Receiving objects: 100% (1018/1018), 761.83 KiB | 5.15 MiB/s, done.
Resolving deltas: 100% (427/427), done.
</code></pre></div></div>

<p>Then sym link it to my skills</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ ln -s /home/builder/Workspaces/claude-code-skills/skills/clean-code-principles /home/builder/.agents/skills/clean-code-principals
</code></pre></div></div>

<p>I tried a couple models in Opencode running in WSL but ultimately found they just got hung up on thinking and quit</p>

<p><a href="/content/images/2026/05/cleancode-01.png"><img src="/content/images/2026/05/cleancode-01.png" alt="/content/images/2026/05/cleancode-01.png" /></a></p>

<p><a href="/content/images/2026/05/cleancode-07.png"><img src="/content/images/2026/05/cleancode-07.png" alt="/content/images/2026/05/cleancode-07.png" /></a></p>

<h2 id="continuedev-in-vs-code">Continue.dev in VS Code</h2>

<p>I had another idea: use the continue.dev plugin in VS Code.</p>

<p>Since that doesn’t have skills (at least my version doesn’t), I instead put the contents in as a rule:</p>

<p><a href="/content/images/2026/05/cleancode-02.png"><img src="/content/images/2026/05/cleancode-02.png" alt="/content/images/2026/05/cleancode-02.png" /></a></p>

<p>Then used it to review a file in a different Python repo:</p>

<p><a href="/content/images/2026/05/cleancode-03.png"><img src="/content/images/2026/05/cleancode-03.png" alt="/content/images/2026/05/cleancode-03.png" /></a></p>

<p>I tried a larger 27B model in “Ask” mode which also worked</p>

<p><a href="/content/images/2026/05/cleancode-04.png"><img src="/content/images/2026/05/cleancode-04.png" alt="/content/images/2026/05/cleancode-04.png" /></a></p>

<p>I also went and tried Gemini Code Assist (GCA) against the skill</p>

<p><a href="/content/images/2026/05/cleancode-05.png"><img src="/content/images/2026/05/cleancode-05.png" alt="/content/images/2026/05/cleancode-05.png" /></a></p>

<p>I noticed it quickly picked up that it was a skill</p>

<p><a href="/content/images/2026/05/cleancode-06.png"><img src="/content/images/2026/05/cleancode-06.png" alt="/content/images/2026/05/cleancode-06.png" /></a></p>

<h2 id="antigravity">Antigravity</h2>

<p>I tried to reference the skill in Antigravity but Aggy is still just looking in the ~/.gemini folder for skills</p>

<p><a href="/content/images/2026/05/cleancode-10.png"><img src="/content/images/2026/05/cleancode-10.png" alt="/content/images/2026/05/cleancode-10.png" /></a></p>

<p>I tried again but this time referenced the exact path</p>

<p><a href="/content/images/2026/05/cleancode-11.png"><img src="/content/images/2026/05/cleancode-11.png" alt="/content/images/2026/05/cleancode-11.png" /></a></p>

<p>which seemed to work</p>

<p><a href="/content/images/2026/05/cleancode-12.png"><img src="/content/images/2026/05/cleancode-12.png" alt="/content/images/2026/05/cleancode-12.png" /></a></p>

<p><a href="/content/images/2026/05/cleancode-13.png"><img src="/content/images/2026/05/cleancode-13.png" alt="/content/images/2026/05/cleancode-13.png" /></a></p>

<h2 id="gemini-cli">Gemini CLI</h2>

<p>I next tried invoking Gemini CLI on the repo</p>

<p><a href="/content/images/2026/05/cleancode-14.png"><img src="/content/images/2026/05/cleancode-14.png" alt="/content/images/2026/05/cleancode-14.png" /></a></p>

<p>It immediately found the skill to activate</p>

<p><a href="/content/images/2026/05/cleancode-15.png"><img src="/content/images/2026/05/cleancode-15.png" alt="/content/images/2026/05/cleancode-15.png" /></a></p>

<p>It made some suggestions</p>

<p><a href="/content/images/2026/05/cleancode-16.png"><img src="/content/images/2026/05/cleancode-16.png" alt="/content/images/2026/05/cleancode-16.png" /></a></p>

<p>Which came back with several suggestions</p>

<p><a href="/content/images/2026/05/cleancode-17.png"><img src="/content/images/2026/05/cleancode-17.png" alt="/content/images/2026/05/cleancode-17.png" /></a></p>

<p><a href="/content/images/2026/05/cleancode-18.png"><img src="/content/images/2026/05/cleancode-18.png" alt="/content/images/2026/05/cleancode-18.png" /></a></p>

<p>And it really didn’t use that many tokens</p>

<p><a href="/content/images/2026/05/cleancode-19.png"><img src="/content/images/2026/05/cleancode-19.png" alt="/content/images/2026/05/cleancode-19.png" /></a></p>

<h1 id="summary">Summary</h1>

<p><a href="https://fallow.tools/">Fallow</a> worked pretty well.  I found the dead-code feature particularly handy and will likely use this for future JS/TS projects.  However, the lack of other language support limited it’s usage for me.</p>

<p>I like skills, I do.  And the <a href="https://github.com/sammasak/claude-code-skills">claude-code-skills</a> I used worked great, but I still find skill support in these local model tools isn’t fantastic.  They work great with things like Copilot and Gemini CLI, but not as well with continue.dev in VS Code and Antigravity (Aggy).</p>

<p>I feel like I’m not quite “doing it right” with the local LLM tools so I plan to keep working on those until I find the magic sauce.</p>]]></content><author><name>Isaac Johnson</name></author><category term="GeminiCLI" /><category term="Fallow" /><category term="Opensource" /><category term="Skills" /><category term="Opencode" /><category term="Pi" /><category term="GenAI" /><summary type="html"><![CDATA[I recently came across this YT review of Fallow which is easy to run and similar to desloppify is meant to help cleanup vibe code (but also just code in general). In this post I’ll set it up and explore some of its features and touch on some of the paid ones as well.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/05/fallowosbg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/05/fallowosbg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Gemini CLI Pipeline CICD Extension</title><link href="https://freshbrewed.science/2026/05/05/googlecicd.html" rel="alternate" type="text/html" title="Gemini CLI Pipeline CICD Extension" /><published>2026-05-05T01:00:01+00:00</published><updated>2026-05-05T01:00:01+00:00</updated><id>https://freshbrewed.science/2026/05/05/googlecicd</id><content type="html" xml:base="https://freshbrewed.science/2026/05/05/googlecicd.html"><![CDATA[<p>In the very last session of GCP Next, I saw a demonstration of their new <a href="https://github.com/gemini-cli-extensions/cicd">CICD extension</a>.  They linked up a basic Python app with buildkit to CloudRun for a simple CD approach, but then showed a much more comprehensive CICD pipeline using Cloud Build and Cloud Deploy that even had an approval gate.  I was pretty impressed.</p>

<p>Though, if you know me, you know I waited for questions and asked about non-Github/Gitlab/Bitbucket options as CloudBuild only supports those major SaaS providers.</p>

<p>Let’s do this together to see how well it works</p>

<h2 id="gemini-cli">Gemini CLI</h2>

<p>First, a lot of folks use Claude, so for y’all, you can do:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ claude plugin marketplace add https://github.com/gemini-cli-extensions/cicd.git#v0.2.2
$ claude plugin install cicd
</code></pre></div></div>

<p>Since I’m using Gemini CLI, I’ll just add by extensions (which will also check for updates later)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gemini extensions install https://github.com/gemini-cli-extensions/cicd
Installing extension "cicd".
This extension will run the following MCP servers:
  * cicd-mcp (local): /tmp/gemini-extensionbp1hc7/cicd-mcp-server

Agent Skills:

This extension will install the following agent skills:

  * google-cicd-terraform: Architect, provision, and troubleshoot production-grade Google Cloud infrastructure using Terraform and OpenTofu. Use to design landing zones (Shared VPCs, Folders), deploy core services (GKE, Cloud Run, Cloud SQL), implement IAM least-privilege, and manage GCS-backed state. Enforces Google’s Cloud Foundation Fabric patterns and rigorous validation protocols to ensure secure, idempotent, and scalable deployments across environments.
    (Source: /tmp/gemini-extensionbp1hc7/skills/google-cicd-terraform/SKILL.md) (2 items in directory)

  * google-cicd-release-orchestration: Design Cloud Deploy delivery pipelines and manage releases when deploying applications to Cloud Run and Google Kubernetes Engine (GKE). Use when users want to deploy their applications to multiple environments (e.g. dev and prod), leverage deployment strategies (i.e. canary), or rollback (manually or automatically) when there are issues deploying their application.

    (Source: /tmp/gemini-extensionbp1hc7/skills/google-cicd-release-orchestration/SKILL.md) (2 items in directory)

  * google-cicd-pipeline-design: Design and implement a Google Cloud based CI/CD pipeline. Use when the user wants to build a new pipeline, design an architecture on GCP.
    (Source: /tmp/gemini-extensionbp1hc7/skills/google-cicd-pipeline-design/SKILL.md) (2 items in directory)

  * google-cicd-deploy: Deploy an application to Google Cloud. Use when the user wants to deploy a new application on GCP.
    (Source: /tmp/gemini-extensionbp1hc7/skills/google-cicd-deploy/SKILL.md) (2 items in directory)


The extension you are about to install may have been created by a third-party developer and sourced from a public repository. Google does not vet, endorse, or guarantee the functionality or security of extensions. Please carefully inspect any extension and its source code before installing to understand the permissions it requires and the actions it may perform.

Agent skills inject specialized instructions and domain-specific knowledge into the agent's system prompt. This can change how the agent interprets your requests and interacts with your environment. Review the skill definitions at the location(s) provided below to ensure they meet your security standards.
Do you want to continue? [Y/n]: Y
Extension "cicd" installed successfully and enabled.
</code></pre></div></div>

<p>The extension will not be asking for credentials. It assumed you are app logged in, so do:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud auth login
$ gcloud auth application-default login
</code></pre></div></div>

<h1 id="making-the-app">Making the app</h1>

<p>Before I even start, let’s get a valid DESIGN.md as I have yet to do that.
It is a standard, but we can derive it with stitch from an existing website (that I just mucked about to build the first time).</p>

<p>From the project page, we will start with a Design.md</p>

<p><a href="/content/images/2026/05/googlecicd-01.png"><img src="/content/images/2026/05/googlecicd-01.png" alt="/content/images/2026/05/googlecicd-01.png" /></a></p>

<p>I can now export a Zip file which has a DESIGN.md</p>

<p><a href="/content/images/2026/05/googlecicd-02.png"><img src="/content/images/2026/05/googlecicd-02.png" alt="/content/images/2026/05/googlecicd-02.png" /></a></p>

<p>I then copied it into the local folder where we’ll work on the app</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/letsfishapp$ rm ./stitch_wilderness_trip_coordinator_dashboard\ \(1\).zip
(base) builder@LuiGi:~/Workspaces/letsfishapp$ ls
DESIGN.md  code.html  screen.png
(base) builder@LuiGi:~/Workspaces/letsfishapp$ ls -l
total 620
-rw-rw-r-- 1 builder builder   6973 Apr 24  2026 DESIGN.md
-rw-rw-r-- 1 builder builder  11816 Apr 24  2026 code.html
-rw-rw-r-- 1 builder builder 611949 Apr 24  2026 screen.png
</code></pre></div></div>

<h2 id="the-cicd-extension">The CICD Extension</h2>

<p>We can see the new CICD skills that were added when I ran <code class="language-plaintext highlighter-rouge">gemini extensions install https://github.com/gemini-cli-extensions/cicd</code> earlier:</p>

<p><a href="/content/images/2026/05/googlecicd-03.png"><img src="/content/images/2026/05/googlecicd-03.png" alt="/content/images/2026/05/googlecicd-03.png" /></a></p>

<p>But first, let’s use Gemini CLI to make a quick app (in order to use those skills).</p>

<p><a href="/content/images/2026/05/googlecicd-04.png"><img src="/content/images/2026/05/googlecicd-04.png" alt="/content/images/2026/05/googlecicd-04.png" /></a></p>

<p>Because I have the Nanobanana MCP server, Gemini CLI will create a hero image on the fly</p>

<p><a href="/content/images/2026/05/googlecicd-05.png"><img src="/content/images/2026/05/googlecicd-05.png" alt="/content/images/2026/05/googlecicd-05.png" /></a></p>

<p>and create a webpage based on the design</p>

<p><a href="/content/images/2026/05/googlecicd-06.png"><img src="/content/images/2026/05/googlecicd-06.png" alt="/content/images/2026/05/googlecicd-06.png" /></a></p>

<p>which looks good</p>

<p><a href="/content/images/2026/05/googlecicd-07.png"><img src="/content/images/2026/05/googlecicd-07.png" alt="/content/images/2026/05/googlecicd-07.png" /></a></p>

<p>I used a few more prompts to build out the other pages</p>

<blockquote>
  <p>Create the Pricing page for “WildTrack Lite” that has a Community Edition for $0 and a Professional edition for $5/mo.  It should include features like GPS tracking, maps, communications and MFA-backed Federated Identity.  Make sure to use the style guide defined in @DESIGN.md. This same information should be included on the Features page linked from @welcome.html .</p>
</blockquote>

<p><a href="/content/images/2026/05/googlecicd-08.png"><img src="/content/images/2026/05/googlecicd-08.png" alt="/content/images/2026/05/googlecicd-08.png" /></a></p>

<h2 id="deploy">Deploy</h2>

<p>At this point I have <strong>NOT</strong> made a Dockerfile - just three webpages with no real functionality.</p>

<p>Let’s start with the “CICD Deploy” option:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  - google-cicd-deploy
      Deploy an application to Google Cloud. Use when the user wants to deploy a new application on GCP.
</code></pre></div></div>

<p>We can ask the skill to do this and see it will look up some of its references</p>

<p><a href="/content/images/2026/05/googlecicd-09.png"><img src="/content/images/2026/05/googlecicd-09.png" alt="/content/images/2026/05/googlecicd-09.png" /></a></p>

<p>It figured out it was a static site and plans to use Google Cloud Storage.  The skill now wants to use the secrets scanner MCP server</p>

<p><a href="/content/images/2026/05/googlecicd-10.png"><img src="/content/images/2026/05/googlecicd-10.png" alt="/content/images/2026/05/googlecicd-10.png" /></a></p>

<p>It’s now satisfied I have no secrets leaks and looks to pick a project</p>

<p><a href="/content/images/2026/05/googlecicd-11.png"><img src="/content/images/2026/05/googlecicd-11.png" alt="/content/images/2026/05/googlecicd-11.png" /></a></p>

<p>It then just deployed to a bucket and gave me back a URL, <a href="https://storage.googleapis.com/wildtrack-lite-static-7788/welcome.html">https://storage.googleapis.com/wildtrack-lite-static-7788/welcome.html</a></p>

<p><a href="/content/images/2026/05/googlecicd-12.png"><img src="/content/images/2026/05/googlecicd-12.png" alt="/content/images/2026/05/googlecicd-12.png" /></a></p>

<p>Which worked just fine</p>

<p><a href="/content/images/2026/05/googlecicd-13.png"><img src="/content/images/2026/05/googlecicd-13.png" alt="/content/images/2026/05/googlecicd-13.png" /></a></p>

<p>But what if I wanted Federated IdP?  I can do that with a Cloud Run Container</p>

<video muted="" controls="">
    <source src="/content/images/2026/05/googlecicd-14.mp4" type="video/mp4" />
</video>

<p>It moved to using a Cloud Run app and a new Dockerfile</p>

<p><a href="/content/images/2026/05/googlecicd-14.png"><img src="/content/images/2026/05/googlecicd-14.png" alt="/content/images/2026/05/googlecicd-14.png" /></a></p>

<p>If I try and access <a href="https://wildtrack-lite-secure-q5jg7qcghq-uc.a.run.app">https://wildtrack-lite-secure-q5jg7qcghq-uc.a.run.app</a> we’ll see a forbidden page</p>

<p><a href="/content/images/2026/05/googlecicd-15.png"><img src="/content/images/2026/05/googlecicd-15.png" alt="/content/images/2026/05/googlecicd-15.png" /></a></p>

<p>I did as it suggested and flipped the IAP setting on the service</p>

<video muted="" controls="">
    <source src="/content/images/2026/05/googlecicd-16.mp4" type="video/mp4" />
</video>

<p>I now see</p>

<p><a href="/content/images/2026/05/googlecicd-16.png"><img src="/content/images/2026/05/googlecicd-16.png" alt="/content/images/2026/05/googlecicd-16.png" /></a></p>

<p>Let’s add my own email as a user who can access.  We’ll check the box and click add principal</p>

<p><a href="/content/images/2026/05/googlecicd-17.png"><img src="/content/images/2026/05/googlecicd-17.png" alt="/content/images/2026/05/googlecicd-17.png" /></a></p>

<p>I added the role</p>

<p><a href="/content/images/2026/05/googlecicd-18.png"><img src="/content/images/2026/05/googlecicd-18.png" alt="/content/images/2026/05/googlecicd-18.png" /></a></p>

<p>And verified it was listed</p>

<p><a href="/content/images/2026/05/googlecicd-19.png"><img src="/content/images/2026/05/googlecicd-19.png" alt="/content/images/2026/05/googlecicd-19.png" /></a></p>

<p>Now, the root URL shows an NGinx welcome page, but “welcome.html” works</p>

<p><a href="/content/images/2026/05/googlecicd-20.png"><img src="/content/images/2026/05/googlecicd-20.png" alt="/content/images/2026/05/googlecicd-20.png" /></a></p>

<h2 id="staging">Staging</h2>

<p>First, I need to create a GIT repo</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/letsfishapp$ git init
Initialized empty Git repository in /home/builder/Workspaces/letsfishapp/.git/
(base) builder@LuiGi:~/Workspaces/letsfishapp$ git checkout -b main
Switched to a new branch 'main'
(base) builder@LuiGi:~/Workspaces/letsfishapp$ git status
On branch main

No commits yet

Untracked files:
  (use "git add &lt;file&gt;..." to include in what will be committed)
        .dockerignore
        DESIGN.md
        Dockerfile
        code.html
        features.html
        index.html
        nanobanana-output/
        pricing.html
        screen.png

nothing added to commit but untracked files present (use "git add" to track)
(base) builder@LuiGi:~/Workspaces/letsfishapp$ git add -A
(base) builder@LuiGi:~/Workspaces/letsfishapp$ git commit -m 'initial'
[main (root-commit) f9c8868] initial
 9 files changed, 1034 insertions(+)
 create mode 100644 .dockerignore
 create mode 100644 DESIGN.md
 create mode 100644 Dockerfile
 create mode 100644 code.html
 create mode 100644 features.html
 create mode 100644 index.html
 create mode 100644 nanobanana-output/modern_sleek_hero_image_for_a_wi.png
 create mode 100644 pricing.html
 create mode 100644 screen.png
(base) builder@LuiGi:~/Workspaces/letsfishapp$
</code></pre></div></div>

<p>I’ll make a repo in Forgejo</p>

<p><a href="/content/images/2026/05/googlecicd-21.png"><img src="/content/images/2026/05/googlecicd-21.png" alt="/content/images/2026/05/googlecicd-21.png" /></a></p>

<p>and push our content up</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/letsfishapp$ git remote add origin https://forgejo.freshbrewed.science/builderadmin/letsfishapp.git
(base) builder@LuiGi:~/Workspaces/letsfishapp$ git push -u origin --all
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 16 threads
Compressing objects: 100% (12/12), done.
Writing objects: 100% (12/12), 1.35 MiB | 573.00 KiB/s, done.
Total 12 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: . Processing 1 references
remote: Processed 1 references in total
To https://forgejo.freshbrewed.science/builderadmin/letsfishapp.git
 * [new branch]      main -&gt; main
branch 'main' set up to track 'origin/main'.
(base) builder@LuiGi:~/Workspaces/letsfishapp$
</code></pre></div></div>

<p>Let’s see if it can handle non-Github/Gitlab/Bitbucket</p>

<p><a href="/content/images/2026/05/googlecicd-22.png"><img src="/content/images/2026/05/googlecicd-22.png" alt="/content/images/2026/05/googlecicd-22.png" /></a></p>

<p><a href="/content/images/2026/05/googlecicd-23.png"><img src="/content/images/2026/05/googlecicd-23.png" alt="/content/images/2026/05/googlecicd-23.png" /></a></p>

<p>It seemed to have some issues setting up the trigger</p>

<p><a href="/content/images/2026/05/googlecicd-24.png"><img src="/content/images/2026/05/googlecicd-24.png" alt="/content/images/2026/05/googlecicd-24.png" /></a></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gcloud developer-connect connections create forgejo-connection --location=us-central1 --git-repository-uri="https://forgejo.freshbrewed.science/builderadmin/letsfishapp.git" --project=myanthosproject2
</code></pre></div></div>

<p>However, in digging, the manual connection failed:</p>

<p><a href="/content/images/2026/05/googlecicd-25.png"><img src="/content/images/2026/05/googlecicd-25.png" alt="/content/images/2026/05/googlecicd-25.png" /></a></p>

<p>As an aside, I checked back later and saw just 28c charge over the week so this thus far has barely incurred any costs</p>

<p><a href="/content/images/2026/05/googlecicd-26.png"><img src="/content/images/2026/05/googlecicd-26.png" alt="/content/images/2026/05/googlecicd-26.png" /></a></p>

<p>And even following the specific instructions:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Here are the specific gcloud commands you should run manually to finish the setup:

  1. Create the Git Connection
  This creates the link to your Forgejo instance. Replace [CONNECTION_ID] with a name like forgejo-connection.

   1 gcloud developer-connect connections create [CONNECTION_ID] \
   2     --location=us-central1 \
   3     --git-repository-uri="https://forgejo.freshbrewed.science/builderadmin/letsfishapp.git" \
   4     --project=myanthosproject2

  2. Create the Repository Link
  Once the connection is established, link the specific repository:

   1 gcloud developer-connect connections git-repository-links create wildtrack-repo-link \
   2     --connection=[CONNECTION_ID] \
   3     --location=us-central1 \
   4     --git-repository-uri="https://forgejo.freshbrewed.science/builderadmin/letsfishapp.git" \
   5     --project=myanthosproject2

  3. Create the Cloud Build Trigger
  Finally, create the trigger that uses the cloudbuild.yaml file I created:

   1 gcloud alpha builds triggers create git \
   2     --name="wildtrack-lite-push-to-main" \
   3     --location=us-central1 \
   4     --repository="projects/myanthosproject2/locations/us-central1/connections/[CONNECTION_ID]/gitRepositoryLinks/wildtrack-repo-link" \
   5     --branch-pattern="^main$" \
   6     --build-config="cloudbuild.yaml" \
   7     --service-account="projects/myanthosproject2/serviceAccounts/forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
   8     --project=myanthosproject2
</code></pre></div></div>

<p>But there is no way to add a manual connection that is outside the major SaaS providers when one goes to <a href="https://console.cloud.google.com/developer-connect/git-repository-links?referrer=search">Developer Connect</a></p>

<p><a href="/content/images/2026/05/googlecicd-27.png"><img src="/content/images/2026/05/googlecicd-27.png" alt="/content/images/2026/05/googlecicd-27.png" /></a></p>

<p>I spoke with the Program Manager at Google for the product and she was aware that folks have sought some kind of solution for tooling like AzDO.</p>

<h2 id="using-a-common-saas-option">Using a common SaaS option</h2>

<p>Since there are lots of Demos of Github already, let’s create a blank repo/project in Gitlab for this</p>

<p><a href="/content/images/2026/05/googlecicd-28.png"><img src="/content/images/2026/05/googlecicd-28.png" alt="/content/images/2026/05/googlecicd-28.png" /></a></p>

<p>I then will add a push mirror to Gitlab using a PAT</p>

<p><a href="/content/images/2026/05/googlecicd-29.png"><img src="/content/images/2026/05/googlecicd-29.png" alt="/content/images/2026/05/googlecicd-29.png" /></a></p>

<p>Then force a sync</p>

<p><a href="/content/images/2026/05/googlecicd-30.png"><img src="/content/images/2026/05/googlecicd-30.png" alt="/content/images/2026/05/googlecicd-30.png" /></a></p>

<p>Now I see the contents (<em>btw, later I will realize i set the push for a different Forgejo repo - this is for a future offenquelle project - still works but i will realize it later</em>)</p>

<p><a href="/content/images/2026/05/googlecicd-31.png"><img src="/content/images/2026/05/googlecicd-31.png" alt="/content/images/2026/05/googlecicd-31.png" /></a></p>

<p>I can now using some new tokens to create a connection</p>

<p><a href="/content/images/2026/05/googlecicd-32.png"><img src="/content/images/2026/05/googlecicd-32.png" alt="/content/images/2026/05/googlecicd-32.png" /></a></p>

<p>Though using Project-scoped tokens failed (even though the page said they would work)</p>

<p><a href="/content/images/2026/05/googlecicd-33.png"><img src="/content/images/2026/05/googlecicd-33.png" alt="/content/images/2026/05/googlecicd-33.png" /></a></p>

<p>But the full user based classic tokens worked and let me pick a repository</p>

<p><a href="/content/images/2026/05/googlecicd-34.png"><img src="/content/images/2026/05/googlecicd-34.png" alt="/content/images/2026/05/googlecicd-34.png" /></a></p>

<p>Now I have a proxy connection set to Gitlab in us-central1 named <code class="language-plaintext highlighter-rouge">isaac.johnson-letsfishapp</code></p>

<p><a href="/content/images/2026/05/googlecicd-35.png"><img src="/content/images/2026/05/googlecicd-35.png" alt="/content/images/2026/05/googlecicd-35.png" /></a></p>

<p>This is going to now get a bit messy.  I need to add a trigger but first I need to give myself access to the service account I created</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud iam service-accounts add-iam-policy-binding \
    forgejo-publisher@myanthosproject2.iam.gserviceaccount.com \
    --member="user:isaac.johnson@gmail.com" \
    --role="roles/iam.serviceAccountUser" \
    --project=myanthosproject2
</code></pre></div></div>

<p>Then give the SA permission for Cloud Builder Build permission</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/cloudbuild.builds.builder"
</code></pre></div></div>

<p>Lastly, grant access to the repository via the service connect</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/cloudbuild.connectionViewer"

gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/cloudbuild.repositoryViewer"

gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/eventarc.eventReceiver"

gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor"

gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/logging.logWriter"

gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/storage.admin"

gcloud iam service-accounts add-iam-policy-binding \
    forgejo-publisher@myanthosproject2.iam.gserviceaccount.com \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/iam.serviceAccountTokenCreator" \
    --project=myanthosproject2
</code></pre></div></div>

<p>Then</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud iam service-accounts add-iam-policy-binding \
    forgejo-publisher@myanthosproject2.iam.gserviceaccount.com \
    --member="serviceAccount:service-511842454269@gcp-sa-cloudbuild.iam.gserviceaccount.com" \
    --role="roles/iam.serviceAccountUser" \
    --project=myanthosproject2
</code></pre></div></div>

<p>Finally</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud alpha builds triggers create repository "wildtrack-lite-push-to-main" \
--region=us-central1 \
--repository="projects/myanthosproject2/locations/us-central1/connections/isaac.johnson-letsfishapp/repositoryLinks/wildtrack-repo-link" \
--branch-pattern="^main$" \
--build-config="cloudbuild.yaml" \
--service-account="projects/myanthosproject2/serviceAccounts/forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
--project=myanthosproject2
</code></pre></div></div>

<p>It took a lot of iterations to find all the permissions.  Next time I’ll use Logs Explorer and search for</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>protoPayload.status.message=~"PERMISSION_DENIED"
protoPayload.authenticationInfo.principalEmail="isaac.johnson@gmail.com"
</code></pre></div></div>

<p>To figure out what all is missing.</p>

<p>I chased it and chased it and finally gave up.  I could see a connection in Developer Connect but then I cannot use it in Cloud Build</p>

<p><a href="/content/images/2026/05/googlecicd-36.png"><img src="/content/images/2026/05/googlecicd-36.png" alt="/content/images/2026/05/googlecicd-36.png" /></a></p>

<h2 id="do-over">Do over</h2>

<p>I cloned the Gitlab repo locally and tried again</p>

<p><a href="/content/images/2026/05/googlecicd-37.png"><img src="/content/images/2026/05/googlecicd-37.png" alt="/content/images/2026/05/googlecicd-37.png" /></a></p>

<p>It failed with permissions.  I tried a few things</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud projects add-iam-policy-binding myanthosproject2 \
    --member="serviceAccount:offenquelle-deploy-sa@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/artifactregistry.reader"
</code></pre></div></div>

<p>which finally rendered a successful push</p>

<p><a href="/content/images/2026/05/googlecicd-38.png"><img src="/content/images/2026/05/googlecicd-38.png" alt="/content/images/2026/05/googlecicd-38.png" /></a></p>

<p>I can see the app URL</p>

<p><a href="/content/images/2026/05/googlecicd-39.png"><img src="/content/images/2026/05/googlecicd-39.png" alt="/content/images/2026/05/googlecicd-39.png" /></a></p>

<p>Which shows an error</p>

<p><a href="/content/images/2026/05/googlecicd-40.png"><img src="/content/images/2026/05/googlecicd-40.png" alt="/content/images/2026/05/googlecicd-40.png" /></a></p>

<p>If you have permissions issues, you may need to add yourself as a “Cloud Run Invoker” (however this should be public already) by finding your service (here I’ll use a different one)</p>

<p><a href="/content/images/2026/05/googlecicd-41.png"><img src="/content/images/2026/05/googlecicd-41.png" alt="/content/images/2026/05/googlecicd-41.png" /></a></p>

<p>Then grant myself “Cloud Run Invoker”</p>

<p><a href="/content/images/2026/05/googlecicd-42.png"><img src="/content/images/2026/05/googlecicd-42.png" alt="/content/images/2026/05/googlecicd-42.png" /></a></p>

<p>If we review <a href="https://freshbrewed.science/2026/04/21/hugo3.html">our steps from Hugo and GCP</a> we can follow a similar pattern and use the auth token or a browser plugin</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  https://offenquelle-511842454269.us-central1.run.app/
</code></pre></div></div>

<p>To allow our browser to know us, we can check the IAP Proxy option in the Security page</p>

<p><a href="/content/images/2026/05/googlecicd-43.png"><img src="/content/images/2026/05/googlecicd-43.png" alt="/content/images/2026/05/googlecicd-43.png" /></a></p>

<p>And then allow yourself access to the IAP proxy for that given service</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud beta iap web add-iam-policy-binding --resource-type=cloud-run --service=offenquelle --region=us-central1 --member='user:isaac.johnson@gmail.com' --role='roles/iap.httpsResourceAccessor'
Updated IAM policy for cloud run [projects/511842454269/iap_web/cloud_run-us-central1/services/offenquelle].
</code></pre></div></div>

<p>And yes, I realize the now rendering page is not the “Wildtrack Lite” page as I errantly set the Gitlab to be a replica of a different private Git repo</p>

<p><a href="/content/images/2026/05/googlecicd-44.png"><img src="/content/images/2026/05/googlecicd-44.png" alt="/content/images/2026/05/googlecicd-44.png" /></a></p>

<h2 id="costs">Costs</h2>

<p>When I circle back on my costs, I see a rather large spike on Cloud Deploy</p>

<p><a href="/content/images/2026/05/googlecicd-45.png"><img src="/content/images/2026/05/googlecicd-45.png" alt="/content/images/2026/05/googlecicd-45.png" /></a></p>

<p>The reason is we are charged US$5/mo per additional pipeline beyond 1.  So that means every pipeline (save for the first) <a href="https://cloud.google.com/deploy/pricing">costs you an extra $5</a>.  That seems awfully steep:</p>

<p><a href="/content/images/2026/05/googlecicd-46.png"><img src="/content/images/2026/05/googlecicd-46.png" alt="/content/images/2026/05/googlecicd-46.png" /></a></p>

<p>This was caused by trying to do pipelines two different ways (the Wildtrack Lite that failed because it couldn’t do Forgejo/Gitea and the new Offenquelle one)</p>

<p><a href="/content/images/2026/05/googlecicd-47.png"><img src="/content/images/2026/05/googlecicd-47.png" alt="/content/images/2026/05/googlecicd-47.png" /></a></p>

<p>It is not a graduated payment either.  Create a pipeline, pay $5.  However, if you delete one and create one <em>with the same name</em> they won’t double charge - so bare that in mind (e.g. if you do a lot of demos, perhaps name them “myclouddeploypipeline”) and just re-use it.</p>

<p>I don’t like paying for things unnecessarily so I’ll delete these pipelines.</p>

<p><a href="/content/images/2026/05/googlecicd-48.png"><img src="/content/images/2026/05/googlecicd-48.png" alt="/content/images/2026/05/googlecicd-48.png" /></a></p>

<p>You can chose to delete targets at the same time, or come back later to do that</p>

<p><a href="/content/images/2026/05/googlecicd-49.png"><img src="/content/images/2026/05/googlecicd-49.png" alt="/content/images/2026/05/googlecicd-49.png" /></a></p>

<h1 id="pipelines">Pipelines</h1>

<p>We spent a bit <em>too</em> long fighting Cloud Deploy and I’m not keen on the pricing.</p>

<p>We could easily avoid these costs by just using a pipeline system in our revision control platform.</p>

<p>Whether we are talking about Gitlab</p>

<p><a href="/content/images/2026/05/googlecicd-50.png"><img src="/content/images/2026/05/googlecicd-50.png" alt="/content/images/2026/05/googlecicd-50.png" /></a></p>

<p>Or Github</p>

<p><a href="/content/images/2026/05/googlecicd-51.png"><img src="/content/images/2026/05/googlecicd-51.png" alt="/content/images/2026/05/googlecicd-51.png" /></a></p>

<p>Or Forgejo/Gitea</p>

<p><a href="/content/images/2026/05/googlecicd-52.png"><img src="/content/images/2026/05/googlecicd-52.png" alt="/content/images/2026/05/googlecicd-52.png" /></a></p>

<p>You can use <a href="https://developer.harness.io/docs/open-source/installation/quick-start">Harness or Harness OS</a> or <a href="https://dev.azure.com">Azure DevOps</a></p>

<p><a href="/content/images/2026/05/googlecicd-53.png"><img src="/content/images/2026/05/googlecicd-53.png" alt="/content/images/2026/05/googlecicd-53.png" /></a></p>

<p>There are just so many free and open choices (and yes, you can use junkins. I’m not going to though).</p>

<p><a href="/content/images/2026/05/googlecicd-54.png"><img src="/content/images/2026/05/googlecicd-54.png" alt="/content/images/2026/05/googlecicd-54.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>I found the <a href="https://github.com/gemini-cli-extensions/cicd">Pipeline CICD</a> did a fine job if we are either doing a direct Cloud Run deploy (which effectively packages and pushes to a bucket which then creates the Cloud Run instance) or a Cloud Deploy pipeline, provided you stick to a well-known SaaS provider.</p>

<p>I found the Cloud Deploy still had plenty of nuances and I could tell it was stringing up a lot of components as it went along to create connections to Gitlab, then service account permissions (which in Gemini CLI I needed to use tab to go in and confirm).</p>

<p>After I fought through the Cloud Deploy permissions issues, I noticed the cost spike and it made me want to abandon Cloud Deploy all-together, especially when we consider there are so many other available pipeline styles.</p>

<p>If I needed gates, I would likely consider Github, Gitlab and Azure DevOps (e.g. Using an Azure DevOps “Environment” with a manual approval gate is really easy).</p>

<p>However, the CICD Pipeline Extension isn’t a tacit promotion of Google-only offerings, it’s just meant to help you onboard more seamlessly and to that end, it definitely was easier than hacking about in the UI.</p>

<p>Also, I fought those permissions issues for quite a while.  The unsung hero was really Cloud Assist which is that Gemini logo in the Cloud Console</p>

<p><a href="/content/images/2026/05/googlecicd-55.png"><img src="/content/images/2026/05/googlecicd-55.png" alt="/content/images/2026/05/googlecicd-55.png" /></a></p>

<p>It has the ability to <em>see</em> all my Cloud Setup and find the underlying permissions.  Once I stopped trying to use Gemini in the browser with deep thinking and use Cloud Assist - which could actually go check logs and permissions, I moved along a lot faster.</p>

<p>Going forward, I will keep the Google CICD extension handy and likely just use it for quick CloudRun invokations when not using standard YAML CICD templates for whatever repository hosting option I’m using at the time.</p>]]></content><author><name>Isaac Johnson</name></author><category term="GeminiCLI" /><category term="CICD" /><category term="CloudRun" /><category term="CloudDeploy" /><category term="GenAI" /><summary type="html"><![CDATA[In the very last session of GCP Next, I saw a demonstration of their new CICD extension. They linked up a basic Python app with buildkit to CloudRun for a simple CD approach, but then showed a much more comprehensive CICD pipeline using Cloud Build and Cloud Deploy that even had an approval gate. I was pretty impressed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/05/geminiclicicdbg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/05/geminiclicicdbg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Gemini CLI Subagents</title><link href="https://freshbrewed.science/2026/04/30/subagents.html" rel="alternate" type="text/html" title="Gemini CLI Subagents" /><published>2026-04-30T10:00:01+00:00</published><updated>2026-04-30T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/30/subagents</id><content type="html" xml:base="https://freshbrewed.science/2026/04/30/subagents.html"><![CDATA[<p>It would seem everyone is getting excited about subagents but me.  When I tested them <a href="/2026/04/09/gemma.html">with Claude Code a few weeks ago</a>, I just saw them as a clever way to get me to blow out my budget to get roughly the same work back.</p>

<p>However, reading <a href="https://medium.com/google-cloud/mastering-gemini-cli-subagents-part-1-a4666091c154">this recent article</a> by <a href="https://iromin.medium.com/">Romim Irani</a>, made me wonder if I was just looking at them wrong.</p>

<p>In this post we will explore subagents in Gemini CLI (something that won’t kill my budget) and see if we can find some value that I wasn’t understanding.</p>

<h1 id="the-problem-with-single-agents">The Problem with single agents</h1>

<p>The point Romin makes in his <a href="https://medium.com/google-cloud/mastering-gemini-cli-subagents-part-1-a4666091c154">Medium article</a> is that when you just use one agent and keep going, that context you build up is carried from one task to another.</p>

<p>I might imagine it as a very good worker who every time you ask a question, gathers all the info in reams of folders and stacks of papers on her desk.  When you ask another followup on something else, that desk is just covered in the old information and keeps piling up.  This can impact cost (in large contexts) but also is quite inefficient.</p>

<h2 id="a-simple-agent">A simple agent</h2>

<p>I’m not just going to copy his specialist, which makes a readme, but I’ll riff on it a bit.</p>

<p>Let’s make a subagent that will make a SYSTEM.md with architecture diagrams.  This is something I like to have in every repo I make.</p>

<p>I have a <a href="https://wildtrack.in">Wilderness App</a> that is not open-source, but is something I try to maintain.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ tree -L 2
.
├── AGENTS.md
├── AUTHSETUP.md
├── Dockerfile
├── about.md
├── client
│   ├── README.md
│   ├── dist
│   ├── eslint.config.js
│   ├── index.html
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── src
│   ├── tsconfig.app.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
├── helm
│   └── wildtrack
├── k8s
│   └── manifests.yaml
├── manfiestbeta.yaml
├── myharborreg.yaml
├── myvalues.yaml
├── myvaluesbeta.yaml
├── scorecard.png
├── screen.png
├── server
│   ├── auth.js
│   ├── database.js
│   ├── email.js
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── server.js
│   └── wildtrack.db
├── versions.md
└── wildsecrets.yml
</code></pre></div></div>

<p>Let’s create a subagent for the architectural diagrams.  As I would want this <em>for all projects</em>, I’m creating it at the root level of my VM</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ mkdir -p ~/.gemini/agents</span>
<span class="s">(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ vi ~/.gemini/agents/system-architect.md</span>
<span class="s">(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ cat ~/.gemini/agents/system-architect.md</span>
<span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">system-architect</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Specialized in analyzing project structures and writing SYSTEM.md files with diagrams.</span>
<span class="na">tools</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">read_file</span>
<span class="pi">-</span> <span class="s">glob</span>
<span class="na">model</span><span class="pi">:</span> <span class="s">inherit</span>
<span class="nn">---</span>

<span class="na">You are a Technical Writer. Your goal is to look at the files in the current directory and sub-directories and update or create a SYSTEM.md that includes</span><span class="pi">:</span>
<span class="s">1. Project Name.</span>
<span class="s">2. Overall Design</span>
<span class="s">3. Primary components of the project</span>
<span class="s">4. A diagram of these components in MermaidJS</span>
<span class="s">5. If possible, authorship information in a markdown table</span>
<span class="s">6. If possible, version and dependencies information in a markdown table</span>

<span class="s">Do not modify any files; only provide the Markdown text in SYSTEM.md.</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>The fields are pretty self-explanatory.  The tools is not required and would get all available tools if you omit it.</p>

<p>When I launched Gemini CLI, I could use <code class="language-plaintext highlighter-rouge">/agents</code> to see my new agent in the list</p>

<p><a href="/content/images/2026/04/subagents-01.png"><img src="/content/images/2026/04/subagents-01.png" alt="/content/images/2026/04/subagents-01.png" /></a></p>

<p>We can also see the new agent listed in tools when we run <code class="language-plaintext highlighter-rouge">/tools</code></p>

<p><a href="/content/images/2026/04/subagents-02.png"><img src="/content/images/2026/04/subagents-02.png" alt="/content/images/2026/04/subagents-02.png" /></a></p>

<p>Using the sub-agent is a bit like sending a message in a chat client, we use <code class="language-plaintext highlighter-rouge">@</code> to direct work to it.</p>

<p>No. not “it”. I don’t like that.  Some of the best Architects I’ve worked with are women.  Let’s direct some work to her.</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/subagents-03.mp4" type="video/mp4" />
</video>

<p>We can see that the tokens used by the agent are called out separately (highlight mine)</p>

<p><a href="/content/images/2026/04/subagents-04.png"><img src="/content/images/2026/04/subagents-04.png" alt="/content/images/2026/04/subagents-04.png" /></a></p>

<p><em>Since this blog is in markdown, I have found MD in MD gets messy. So forgive the images below, but it’s a better way to share.</em></p>

<p>Here we see a pretty accurate Overall Design and Primary Components.</p>

<p><a href="/content/images/2026/04/subagents-05.png"><img src="/content/images/2026/04/subagents-05.png" alt="/content/images/2026/04/subagents-05.png" /></a></p>

<p>VS Code preview (or at least mine) will show the Mermaid code</p>

<p><a href="/content/images/2026/04/subagents-06.png"><img src="/content/images/2026/04/subagents-06.png" alt="/content/images/2026/04/subagents-06.png" /></a></p>

<p>However, I can use <a href="https://mermaid.live/edit#pako:eNp9U99v2jAQ_lcsP0xUohQoAxJNk1pgHVKZGCl7WNiDmxyQLbEj_5hgiP995zioDpPmh-Tu83d3393JJ5qIFGhId5KVe_Iy3XCCR5lXB2zoJM-Aa9JaAUs0eUe-ZRpuNtTx7FnP47UCSeZcg9yyBH683a2EQTB2sc7xbh-M3k8Ehh10bG1SOx7lsxC_VDwxSovCOeSOPCznxMnymE8LVsZPQuxyIGiqSg82oTPBaxrwdMP_aTAC-Rvlt77gIDo_FbY4O5QSlGp0iTXj1Sx6qYrPeFqKjGvl1V8ypUohdXwxbC7blcdZmNyOw_3IpwylrstcsNRPtGCIy9jKKSrzv-qXIFWmNPAEUHmkhWQ78IVPH-NW9PUZt0amTLNXpuDGq1bXj59FwnInKTpivoLcmYY0r_x6Tj7c3n6sF9rEvJ02L6rlOcjt0dGX8ybNrtEh7mvHXV1cxnoFu1legdPHa1Y1SD_vJV0zoN5MhdWToW1agMRNpPhKTpa2oXoPBQ45RDOFLcMgO_EzUpnRIjryhIZaGmhTU6ZMwzRjuK_iAkphdnsablmu0NtJm7p2Ssa_C_HGxKmDnAjDNQ2DNoU0ww0v3JOtXm4VQsMTPSAh6PQHveGoG4zuh-Pe-3GbHmk47Hd6_UEwHAXBsHvfGw_ObfqnqtHtjEeDwD_nv904O0g">Mermaid live</a> to render it</p>

<p><a href="/content/images/2026/04/subagents-07.png"><img src="/content/images/2026/04/subagents-07.png" alt="/content/images/2026/04/subagents-07.png" /></a></p>

<p>The part I question is the auth.  I definitely am using federated IdP to Google for auth so there should be some noted Oauth2 calls there I am not seeing.</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/subagents-08.mp4" type="video/mp4" />
</video>

<p>I can see the Components now note the OAuth2 flow</p>

<p><a href="/content/images/2026/04/subagents-09.png"><img src="/content/images/2026/04/subagents-09.png" alt="/content/images/2026/04/subagents-09.png" /></a></p>

<p>And the <a href="https://mermaid.live/edit#pako:eNp9VFFv2jAQ_iuWHyYqURoCBIimSS3QLlLZMlL2sLAHNzGQLbEj2-lglP--cxxEAtLykvP5u7vv7rvkgCMeU-zijSD5Fr1MVwzBI4tX41jhSZpQplBrQUmk0Af0PVH0ZoUNTj9LL1xKKpDHFBVrEtGf57sFL8AZmlhzqN3eF2o74RC2U6G2UXWoQT5z_luGk0IqnpkDukP3vocMrRryaU7y8InzTUoRmLLkA02ohLMKRlm8YlcNBlS8Af3WFxhE55eEFme7XFApG11CzXAxC17K4jMW5zxhStbq-0TKnAsVngydS3dVw8yLVI_DvNBjAlSXecpJXE80J-AXoaaTleZ_2ftUyEQqyiIKzAPFBdnQOvHpQ9gKvj2DamhKFHklkt7UqlX1w2cekdRQCvaQL0N3RYNaWV4bVxRmO-iGQbQv-FsSA6F6fSOJF_sncb7qodgIPNeNLT308fb2U7UqTV9tW5oX5VoYl9kQA_e9JkwvSL0DLWR5cRLswm1UunBOHy5RpUT1vKd0zYCG972cAbI7FnpM-Z_385AMuFqQMkElEG7jjApYiBg-1oOGrbDa0gy0dsGM6ZpAkB78EaCkUDzYswi7ShS0jYs8JopOEwKaZSen4MVmi901SSWcNkKnrg45YT84PyNBIiomvGAKu-M2pnECizY3f47yB1KGYPeAd9h1-p2BZXXHVs9y-j2728Z77PadzsAeDO1Rt29bznjkOMc2_lvWsDqjvjMadrvjoW33HKfXO_4DMahlcw">mermaid diagram</a> now looks closer to what I expected</p>

<p><a href="/content/images/2026/04/subagents-10.png"><img src="/content/images/2026/04/subagents-10.png" alt="/content/images/2026/04/subagents-10.png" /></a></p>

<p>Lastly, let’s look at the “Authorship” and “Versions and Dependencies” sections</p>

<p><a href="/content/images/2026/04/subagents-11.png"><img src="/content/images/2026/04/subagents-11.png" alt="/content/images/2026/04/subagents-11.png" /></a></p>

<p>I was not expecting the “Feedback Form” link, but it did put a link <a href="https://freshbrewed.science/feedback.html">to my feedback form</a>.</p>

<h3 id="why-authorship-and-dependencies">Why Authorship and Dependencies.</h3>

<p>If you end up working in a larger corporate environment, especially around MedTech or FDA regulated industries finding “SOUP” (Software of Unknown Providence) is a task that needs completion before submission or updates.</p>

<p>That’s where you have to fun through all the code and find versions (and Licenses) of your dependencies to including in your submission details.   If you have GPL’ed code, you have to mitigate that somehow.</p>

<p>It’s also really useful when having to update code for a vulnerability.  Years back I was at a company where there was a libcss critical CVE and we had to find and fix all code.  That involved a lot of searching.</p>

<p>The “Authorship” helps for PR reviews but also, when handing off to other teams later, whom they can address historical questions.  This can also be really handy for new hires who don’t really know everyone and when tasked with updating some code, often have to use git blames to figure out who did what and when.</p>

<h1 id="tweaks">Tweaks</h1>

<p>I realized I wanted this agent to be a bit less creative and a bit more accurate next time.  This can be controlled with “Temperature”.  A rather funny term for controlling how much latitude it has to ‘make stuff up’.  The default is often “1” but can go between “0.0 and 2.0”.</p>

<p>The other field we can update now is “max_turns”.  The default in Gemini is 30, but this is basically how many conversation turns are allowed before it returns.  I’ll bring that down to a more reasonable 10.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ !v
vi ~/.gemini/agents/system-architect.md
(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ cat ~/.gemini/agents/system-architect.md
---
name: system-architect
description: Specialized in analyzing project structures and writing SYSTEM.md files with diagrams.
tools:
- read_file
- glob
model: inherit
temperature: 0.1
max_turns: 10
---

You are a Technical Writer. Your goal is to look at the files in the current directory and sub-directories and update or create a SYSTEM.md that includes:
1. Project Name.
2. Overall Design
3. Primary components of the project
4. A diagram of these components in MermaidJS
5. If possible, authorship information in a markdown table
6. If possible, version and dependencies information in a markdown table

Do not modify any files; only provide the Markdown text in SYSTEM.md.
---
</code></pre></div></div>

<p>You can review <a href="https://geminicli.com/docs/core/subagents/#agent-definition-files">all our option</a>.  For instance, if we have a complicated ask, we may want to add <code class="language-plaintext highlighter-rouge">timeout_mins</code> as well.</p>

<h1 id="allowing-delegation-automatically">Allowing delegation automatically</h1>

<p>We used that <code class="language-plaintext highlighter-rouge">@</code> approach to engage with our sub-agent, but what if you just want Gemini CLI to do it automatically?</p>

<p>It may be enabled already by default by the time you read this, but just to be <em>explicit</em>, I can make sure it is enabled with <code class="language-plaintext highlighter-rouge">enableAgents</code></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span><span class="w"> </span><span class="err">cat</span><span class="w"> </span><span class="err">~/.gemini/settings.json</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"security"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"auth"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"selectedType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"oauth-personal"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"telemetry"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gcp"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"useCliAuth"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"logPrompts"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ui"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"theme"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shades Of Purple"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"general"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"previewFeatures"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"sessionRetention"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"warningAcknowledged"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxAge"</span><span class="p">:</span><span class="w"> </span><span class="s2">"120d"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"experimental"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"skills"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"enableAgents"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"hasSeenIdeIntegrationNudge"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ide"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h1 id="agents-and-mcp-servers">Agents and MCP servers</h1>

<p>I was curious if the agent could leverage an MCP server to do a bit of extra isolated work for me.</p>

<p>I updated my agent to tell it to use Nanobanana to render out a diagram instead of just MermaidJS</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ vi ~/.gemini/agents/system-architect.md
(venv) builder@DESKTOP-QADGF36:~/Workspaces/wildernessapp$ cat ~/.gemini/agents/system-architect.md
---
name: system-architect
description: Specialized in analyzing project structures and writing SYSTEM.md files with diagrams.
tools:
- read_file
- glob
model: inherit
temperature: 0.1
max_turns: 10
---

You are a Technical Writer. Your goal is to look at the files in the current directory and sub-directories and update or create a SYSTEM.md that includes:
1. Project Name.
2. Overall Design
3. Primary components of the project
4. A diagram of these components in MermaidJS
5. A diagram rendered as a PNG image using the nanobanana MCP server
7. If possible, authorship information in a markdown table
8. If possible, version and dependencies information in a markdown table

Do not modify any files; only provide the Markdown text in SYSTEM.md.
---
</code></pre></div></div>

<p>I saw it get into a loop trying and failing to use the tool</p>

<p><a href="/content/images/2026/04/subagents-12.png"><img src="/content/images/2026/04/subagents-12.png" alt="/content/images/2026/04/subagents-12.png" /></a></p>

<p>I killed it after 3 minutes</p>

<p><a href="/content/images/2026/04/subagents-13.png"><img src="/content/images/2026/04/subagents-13.png" alt="/content/images/2026/04/subagents-13.png" /></a></p>

<p>However, I tried doing this again, but this time using the “Pro” model instead of the default “Flash” model and it seemed to quickly complete</p>

<p><a href="/content/images/2026/04/subagents-14.png"><img src="/content/images/2026/04/subagents-14.png" alt="/content/images/2026/04/subagents-14.png" /></a></p>

<p>However, it did <em>not</em> create a PNG diagram, but did add a sequence diagram for the OAuth3 flow</p>

<p><a href="/content/images/2026/04/subagents-15.png"><img src="/content/images/2026/04/subagents-15.png" alt="/content/images/2026/04/subagents-15.png" /></a></p>

<p>I think there was something wrong with my local WSL setup.  I saw errors and even trying to use Nanobanana directly was failing.  I felt something got corrupted locally.</p>

<p>I went to the <a href="https://geminicli.com/extensions/?name=gemini-cli-extensionsnanobanana">extensions page</a> to verify install and did a</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gemini extensions uninstall nanobanana
</code></pre></div></div>

<p>followed by a</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gemini extensions install https://github.com/gemini-cli-extensions/nanobanana
Installing extension "nanobanana".
This extension will run the following MCP servers:
  * nanobanana (local): node /tmp/gemini-extensionOQABYX/mcp-server/dist/index.js
This extension will append info to your gemini.md context using GEMINI.md

The extension you are about to install may have been created by a third-party developer and sourced from a public repository. Google does not vet, endorse, or guarantee the functionality or security of extensions. Please carefully inspect any extension and its source code before installing to understand the permissions it requires and the actions it may perform.
Do you want to continue? [Y/n]: Y
✔ API Key
Your Gemini API key. Get one from https://aistudio.google.com/apikey. … ***************************************
</code></pre></div></div>

<p>Then tried again</p>

<p><a href="/content/images/2026/04/subagents-16.png"><img src="/content/images/2026/04/subagents-16.png" alt="/content/images/2026/04/subagents-16.png" /></a></p>

<p>This time it worked</p>

<p>OAuth sequence diagram:</p>

<p><a href="/content/images/2026/04/a_sequence_diagram_showing_the_g.png"><img src="/content/images/2026/04/a_sequence_diagram_showing_the_g.png" alt="/content/images/2026/04/a_sequence_diagram_showing_the_g.png" /></a></p>

<p>System diagram:</p>

<p><a href="/content/images/2026/04/a_technical_architecture_diagram.png"><img src="/content/images/2026/04/a_technical_architecture_diagram.png" alt="/content/images/2026/04/a_technical_architecture_diagram.png" /></a></p>

<h2 id="trying-mcp-servers-with-subagents-in-linux">Trying MCP servers with Subagents in Linux</h2>

<p>I came back and set this up on my daily driver which is just Ubuntu.</p>

<p>It got into a bad loop with Nanobanana calls from the System Architect agent</p>

<p><a href="/content/images/2026/04/subagents-17.png"><img src="/content/images/2026/04/subagents-17.png" alt="/content/images/2026/04/subagents-17.png" /></a></p>

<h1 id="permissions">PERMISSIONS!</h1>

<p>I finally figured it out.</p>

<p>The next time I ran, it properly asked to create images with Nanobanana</p>

<p><a href="/content/images/2026/04/subagents-18.png"><img src="/content/images/2026/04/subagents-18.png" alt="/content/images/2026/04/subagents-18.png" /></a></p>

<p>and embedded them</p>

<p><a href="/content/images/2026/04/subagents-19.png"><img src="/content/images/2026/04/subagents-19.png" alt="/content/images/2026/04/subagents-19.png" /></a></p>

<p>However, it kept failing to create the actual images because it just couldn’t pass for the NANOBANANA_API_KEY no matter how many times i tried.</p>

<p>Funny enough that if I hard-code my API Key into the nanobanana extension (where xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx is my real API key):</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">(base)</span><span class="w"> </span><span class="err">builder@LuiGi:~/Workspaces/initech$</span><span class="w"> </span><span class="err">cat</span><span class="w"> </span><span class="err">~/.gemini/extensions/nanobanana/gemini-extension.json</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"nanobanana"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.12"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gemini CLI extension for Nano Banana models - generate and manipulate images with text prompts"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"nanobanana"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"${extensionPath}/mcp-server/dist/index.js"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"NANOBANANA_API_KEY"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"contextFileName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GEMINI.md"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"settings"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"API Key"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Your Gemini API key. Get one from https://aistudio.google.com/apikey."</span><span class="p">,</span><span class="w">
      </span><span class="nl">"envVar"</span><span class="p">:</span><span class="w"> </span><span class="s2">"NANOBANANA_API_KEY"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"sensitive"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>I can get it to generate, but the subagent doesn’t know that and leaves it out of the document</p>

<p><a href="/content/images/2026/04/subagents-20.png"><img src="/content/images/2026/04/subagents-20.png" alt="/content/images/2026/04/subagents-20.png" /></a></p>

<p>The diagrams look good though</p>

<p><a href="/content/images/2026/04/subagents-21.png"><img src="/content/images/2026/04/subagents-21.png" alt="/content/images/2026/04/subagents-21.png" /></a></p>

<p>If I stop trying to make the subagent do it and just ask Gemini to find the image and include it,</p>

<p><a href="/content/images/2026/04/subagents-22.png"><img src="/content/images/2026/04/subagents-22.png" alt="/content/images/2026/04/subagents-22.png" /></a></p>

<p>as we see</p>

<p><a href="/content/images/2026/04/subagents-23.png"><img src="/content/images/2026/04/subagents-23.png" alt="/content/images/2026/04/subagents-23.png" /></a></p>

<p>However, I finally did get it to work by:</p>
<ol>
  <li>disabling MCP tools (so it wouldn’t get stuck calling Nanobanana MCP server)</li>
  <li>Setting it to use REST API with the key (and note the URL)</li>
  <li>using the Gemini Pro model</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/initech$ cat ~/.gemini/agents/system-architect.md
---
name: system-architect
description: Specialized in analyzing project structures and writing SYSTEM.md files with diagrams.
tools:
- read_file
- glob
model: inherit
temperature: 0.1
max_turns: 10
---

You are a Technical Writer. Your goal is to look at the files in the current directory and sub-directories and update or create a SYSTEM.md that includes:
1. Project Name.
2. Overall Design
3. Primary components of the project
4. A diagram of these components in MermaidJS
5. A diagram rendered as a PNG image using the nanobanana REST Interface using the x-goog-api-key "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx". see https://ai.google.dev/gemini-api/docs/image-generation#rest_1
6. If possible, authorship information in a markdown table
7. If possible, version and dependencies information in a markdown table

Do not modify any files; only provide the Markdown text in SYSTEM.md.
---
</code></pre></div></div>

<p>With output</p>

<p><a href="/content/images/2026/04/subagents-24.png"><img src="/content/images/2026/04/subagents-24.png" alt="/content/images/2026/04/subagents-24.png" /></a></p>

<h2 id="as-a-skill">As a Skill</h2>

<p>I rewrote this as a skill</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat ~/.agents/skills/system-architect/SKILL.md
---
name: system-architect
description: When I ask for a system diagram, or updating the system diagram, follow these rules.  Should be used in analyzing project structures and writing SYSTEM.md files with diagrams.
metadata:
  copyright: Copyright Freshbrewed. 2026
  version: "0.0.1"
---

# System Architect Skill

This skill defines how we create and update system architecture diagrams saved in a top level SYSTEM.md using markdown

# When to use this skill

Use this skill when:
- Creating a new SYSTEM.md
- Updating SYSTEM.md

# Requirements

- You are a Technical Writer.

##  SYSTEM.md

Your goal is to look at the files in the current directory and sub-directories and update or create a SYSTEM.md with:

1. Project Name.
2. Overall Design
3. Primary components of the project
4. A diagram of these components in MermaidJS
5. If possible, authorship information in a markdown table
6. If possible, version and dependencies information in a markdown table

Example mermaid architecture:
```mermaid
graph TB
    Client --&gt; API[FastAPI/FastMCP]
    API --&gt; Service[Business Logic]
    Service --&gt; DB[(Database)]
    API --&gt; External[External Services]
</code></pre></div></div>

<p>Do not modify any files; only provide the Markdown text in SYSTEM.md.
```</p>

<p>Then tried it in a directory with a game</p>

<p><a href="/content/images/2026/04/subagents-25.png"><img src="/content/images/2026/04/subagents-25.png" alt="/content/images/2026/04/subagents-25.png" /></a></p>

<p>As you can see, it started to write files it was not supposed to.  That was with Qwen3 and Pi</p>

<p>Opencode using Gemma4 just times out and does nothing</p>

<p><a href="/content/images/2026/04/subagents-26.png"><img src="/content/images/2026/04/subagents-26.png" alt="/content/images/2026/04/subagents-26.png" /></a></p>

<p>However, MiniMax M2.5 Free worked well with the skill</p>

<p><a href="/content/images/2026/04/subagents-27.png"><img src="/content/images/2026/04/subagents-27.png" alt="/content/images/2026/04/subagents-27.png" /></a></p>

<p>I had to run it twice as the first time it didnt actually save the file.</p>

<p><a href="/content/images/2026/04/subagents-28.png"><img src="/content/images/2026/04/subagents-28.png" alt="/content/images/2026/04/subagents-28.png" /></a></p>

<p>Here is the rendered diagram</p>

<p><a href="/content/images/2026/04/subagents-29.png"><img src="/content/images/2026/04/subagents-29.png" alt="/content/images/2026/04/subagents-29.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>It would seem sub-agents are useful, but the agent to MCP server path isn’t so great.  Seems it fails to pass forward environment variables which some MCP tools need.</p>

<p>There is likely a solution to this that eludes me today, but certainly we can use REST calls from the agent which means if there is a RESTful approach, we can succeed as we did above.</p>

<h2 id="addendum">Addendum</h2>

<p>I wrote this before GCP Next26.  During the conference I asked some of the Googlers behind Gemini CLI about sub-agents.  They are, by design, operating in a sandbox.  They are really meant to “go do just that thing” in a bit of a sandbox.</p>

<p>They mentioned that perhaps, for security reasons, they locked it down too much and would revisit those decisions.  But once I realized the pattern was intended to be self-contained minions, it made a lot more sense as to why sub-agents couldn’t just reach back to operate on my MCP servers.</p>]]></content><author><name>Isaac Johnson</name></author><category term="gemini" /><category term="agents" /><category term="genai" /><category term="subagents" /><summary type="html"><![CDATA[It would seem everyone is getting excited about subagents but me. When I tested them with Claude Code a few weeks ago, I just saw them as a clever way to get me to blow out my budget to get roughly the same work back.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/geminisubagentsbg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/geminisubagentsbg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">MAI part 2: Transcribe and Voice, image 2e</title><link href="https://freshbrewed.science/2026/04/28/mai2.html" rel="alternate" type="text/html" title="MAI part 2: Transcribe and Voice, image 2e" /><published>2026-04-28T01:00:01+00:00</published><updated>2026-04-28T01:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/28/mai2</id><content type="html" xml:base="https://freshbrewed.science/2026/04/28/mai2.html"><![CDATA[<p>In my <a href="/2026/04/13/aiimage.html">last MAI post</a> we just focused on the MAI-Image-2 image generation model.  It was basic but functional.</p>

<p>In the time since, Microsoft has rolled out “MAI-Transcribe-1” and “MAI-Voice-1”, an audio transcription model and text-to-speech model</p>

<p><a href="/content/images/2026/04/mai2-01.png"><img src="/content/images/2026/04/mai2-01.png" alt="/content/images/2026/04/mai2-01.png" /></a></p>

<h1 id="transcribe">Transcribe</h1>

<p>My first attempt to drop a lecture series in failed as the max size seems to be 10Mb</p>

<p><a href="/content/images/2026/04/mai2-02.png"><img src="/content/images/2026/04/mai2-02.png" alt="/content/images/2026/04/mai2-02.png" /></a></p>

<p>I reduced my recording to 11 minutes and set the bitrate to 110kbps VBR and tried again (this time it was under 10Mb)</p>

<p>I just got an invalid request error</p>

<p><a href="/content/images/2026/04/mai2-03.png"><img src="/content/images/2026/04/mai2-03.png" alt="/content/images/2026/04/mai2-03.png" /></a></p>

<p>I reduced further to 5 minutes..  This time it worked!</p>

<p><a href="/content/images/2026/04/mai2-04.png"><img src="/content/images/2026/04/mai2-04.png" alt="/content/images/2026/04/mai2-04.png" /></a></p>

<p>In fairness, I am using a cassette tape recording from the 1990s of my Grandfather I imported to the PC so the fidelity isn’t great.</p>

<blockquote>
  <p>In other words, I’m going to set something down on the record so that you’ll know right from the beginning, and that is that I’m not an expert in the devotional Christian classics. That would take a lifetime, and if one would take a lifetime, one would become an expert in only maybe a couple of these devotional Christian classics. One could spend a lifetime in Augustine’s Confessions and in the life of Augustine, and at the end of a lifetime, you’re still almost a beginner because you’re dealing with a man of fantastic genius. But I am a learner and I am a believer, not just a Christian believer, but I’m also a strong believer in the worth of the devotional Christian classics. I was very pleased. The secretary sent me your bulletin from last week and also the church newsletter. And as I was reading the newsletter, I thought, “Wow, John is really on target on this subject.” And that this is the season that we get closer to Jesus. And then all at once he introduced the Christian Classics in my presentation. And I was amazed how close we were on that theme. And that’s the only intention for wanting to know the Christian Classics. And by the time this morning is over, I think I’ll make the the proof very clear. But I’m absolutely convinced regarding the possibility of growing spiritually if we invest ourselves in reading and living with these significant books. Now, we can’t read them all in one day. We know that. But we have years before us, and it depends on the kind of commitment that we make. Let me share with you a letter that one of our members received from Chuck Swindoll. Chuck Swindoll, as you know, is an outstanding preacher. I think he lives in Texas now. But I’m not sure of the question that she wrote, but here’s the letter she received. And Jean will appreciate this because he knows her husband. This is Marilyn Ash that received this letter from Dr. Swindoll. “Dear Marilyn, few characteristics are more disheartening about our times than shallowness. So many, it seems, are satisfied to live their lives like a rushing river, a mile wide and about three inches deep.” And then he quotes Richard Foster. “Superficiality is the curse of our age. The doctrine of instant satisfaction is a primary spiritual problem. The desperate need today is not for a greater number of intelligent people or gifted people, but for deep people.” Then he goes on to say, “But depth is neither popular nor easily attained, not these days and not in most places of the world, certainly not in America. Having grown up in this great country and having spent the majority of my life here, I have observed that many have achieved the good life and some the fast life, but precious few the deep life. Part of the reason is obvious. Nothing from this world system encourages it. Various media may entreat or fascinate or entertain us, but they do not deepen us, not really. As persuasive as our politicians may be, few of them prompt either depth of character or a life of quiet faith.” And then one last quotation: “Rarely do I hear someone extol the virtues of slowing down, of being still, of mediating, of meditating on the things that matter, like reordering one’s private life and reshaping one’s priorities.” Quite a quotation by a very knowledgeable person. I think this shallowness in the church is a plague. We’re part of a shallowness of an age. When you think of all the things that hurry up, when you turn on your television today, you expect the picture and the sound to be there right now. Compare that with the old television sets where you had to wait for things to warm up. And just think how impulsive we are at stop signs. We hit a lot of stop signs today. Also, a train that was going across the tracks, which delayed us about 10 or 15 minutes. But there’s something that keeps pushing inside. I must say that about 20 years ago, no longer ago than that, in 1969 and ‘70, I took a sabbatical leave. And probably one of the best things I learned on that sabbatical leave was that I wasn’t God. And I learned to slow down. And I haven’t rubbed the motor in front of a stop sign since. But there is a tendency we want to keep pushing. What about going to McDonald’s? We want that food right now. Our whole temperament, the climate of the times is hurry up, fast, fast, fast. Well, we’re in an area that’s not fast, but it is deep. And this makes a difference. You have received, hopefully, the outline of the class sessions. And what did I do with my own outline? Which I– could I have one, please? Thank you.</p>
</blockquote>

<p>I then thought, what about Music?</p>

<p>Early in his life, my younger brother was in Minnesota Boychoir and I had imported this 1995 cassette tape into the computer as audio files.</p>

<p>Let’s try this track “Freedom is Coming”.</p>

<audio controls="" src="/content/images/2026/04/06-FreedomIsComing.mp3"></audio>

<p>It has high voices singing in unison so I was curious how it would handle that.</p>

<p>I tried it twice and it couldn’t figure out the words</p>

<p><a href="/content/images/2026/04/mai2-08.png"><img src="/content/images/2026/04/mai2-08.png" alt="/content/images/2026/04/mai2-08.png" /></a></p>

<p>There is a rare(r) Unkle album with lots of movie tracks in there I have copied to my computer (as it’s rather harder to replace), <a href="https://www.discogs.com/release/265250-UNKLE-Sounds-WW-III-UNKLE-Vs-UNKLE?srsltid=AfmBOoqF2BcX8Y8Rs7DQyssa41sWWKy8B1KUmmvs3LHvGclWpIjP0WiE">Unkle WW III</a></p>

<p>I tried the first track of that:</p>

<p><a href="/content/images/2026/04/mai2-10.png"><img src="/content/images/2026/04/mai2-10.png" alt="/content/images/2026/04/mai2-10.png" /></a></p>

<blockquote>
  <p>Come with us into the realm of imagination. I’m within range of the reconnaissance vessel, but I’m hit on my left front stabilizer. I’m switching circuits to compensate, but it’s gonna take some time. I need those fighters. The fighters are locked to your coordinates. Stay on target for the rendezvous phase. I’m losing stability. I can’t shake it. I’m gonna switch to the– 77, are you there? 77, do you copy? Copy. 77, do you copy? 77, do you copy?</p>
</blockquote>

<p>That sounds about right to me.</p>

<p>I tried another, a section from a 1977 Steven King audiobook “The Shining” as read by Campbell Scott.  I had bought that audio book as MP3s at the time.</p>

<p>Again, it did just fine with newer content</p>

<p><a href="/content/images/2026/04/mai2-11.png"><img src="/content/images/2026/04/mai2-11.png" alt="/content/images/2026/04/mai2-11.png" /></a></p>

<blockquote>
  <ol>
    <li>The Front Porch The Torrance family stood together on the long front porch of the Overlook Hotel as if posing for a family portrait, Danny in the middle, zippered into last year’s fall jacket which was now too small and starting to come out at the elbow, Wendy behind him, with one hand on his shoulder, and Jack to his left, his own hand resting lightly on his son’s head. Mr. Ullman was a step below them, buttoned into an expensive-looking brown mohair overcoat. The sun was entirely behind the mountains now, edging them with gold fire, making the shadows around things look long and purple. The only three vehicles left in the parking lots were the hotel truck, Ullman’s Lincoln Continental, and the battered Torrance VW. “You’ve got your keys, then?” Ullman said to Jack. “And you understand fully about the furnace and the boiler?” Jack nodded, feeling some real sympathy for Ullman. Everything was done for the season—the ball of string was neatly wrapped up until next May twelfth, not a day earlier or later. And Ullman, who was responsible for all of it, and who referred to the hotel in the unmistakable tones of infatuation, could not help looking for loose ends. “I think everything is well in hand,” Jack said. “Good. I’ll be in touch.” But he still lingered for a moment, as if waiting for the wind to take a hand and perhaps gust him down to his car. He sighed. “All right. Have a good winter, Mr. Torrance, Mrs. Torrance.” “You too, Danny.” “Thank you, sir,” Danny said. “I hope you do, too.” “I doubt it,” Ullman repeated, and he sounded sad. “The place in Florida is a dump, if the out-and-out truth is to be spoken. Busy work. The Overlook is my real job. Take good care of it for me, Mr. Torrance.” “I think it will be here when you get back next spring,” Jack said, and a thought flashed through Danny’s mind. “But will we?” And was gone. “Of course. Of course it will.” Ullman looked out toward the playground where the hedge animals were clattering in the wind. Then he nodded once more, in a business-like way. “Goodbye, then.” He walked quickly and prissily across to his car—a ridiculously big one for such a little man—and tucked himself into it. The Lincoln’s motor purred into life, and the taillights flashed as he pulled out of his parking stall. As the car moved away, Jack could read the small sign at the head of the stall: “Reserved for Mr. Ullman, Manager.” “Right,” Jack said softly. They watched until the car was out of sight, headed down the eastern slope. When it was gone, the three of them looked at each other for a silent, almost frightened moment. They were alone. Aspen leaves whirled and skittered in aimless packs across the lawn that was now neatly mowed and tended for no guest’s eyes. There was no one to see the autumn leaves steal across the grass but the three of them. It gave Jack a curious, shrinking feeling, as if his life force had dwindled to a mere spark while the hotel and the grounds had suddenly doubled in size and become sinister, dwarfing them with sullen, inanimate power. Then Wendy said, “Look at you, Doc. Your nose is running like a firehose. Let’s get inside.” And they did, closing the door firmly behind them against the restless whine of the wind.</li>
  </ol>
</blockquote>

<p>I’m a bit of a data hoarder.  I had saved aside old memory cards from phones.  I found a voice memo from an old Nokia that is of terrible quality from 2004:</p>

<audio controls="" src="/content/images/2026/04/64959.wav"></audio>

<p>I can barely make that out, but MAI actually parsed the background talking.</p>

<p><a href="/content/images/2026/04/mai2-12.png"><img src="/content/images/2026/04/mai2-12.png" alt="/content/images/2026/04/mai2-12.png" /></a></p>

<p>My final test was to just use a basic Android app to record a memo, in this case, the summary for this article</p>

<p><a href="/content/images/2026/04/mai2-17.png"><img src="/content/images/2026/04/mai2-17.png" alt="/content/images/2026/04/mai2-17.png" /></a></p>

<p>Then use the MAI playground to convert it to text</p>

<p><a href="/content/images/2026/04/mai2-18.png"><img src="/content/images/2026/04/mai2-18.png" alt="/content/images/2026/04/mai2-18.png" /></a></p>

<h1 id="voice">Voice</h1>

<p>The Voice generator has quite a few styles that roughly translate to emotions</p>

<p><a href="/content/images/2026/04/mai2-05.png"><img src="/content/images/2026/04/mai2-05.png" alt="/content/images/2026/04/mai2-05.png" /></a></p>

<p>And interestingly named voices</p>

<p><a href="/content/images/2026/04/mai2-06.png"><img src="/content/images/2026/04/mai2-06.png" alt="/content/images/2026/04/mai2-06.png" /></a></p>

<p>I’ll try “wave” (?gender British) with “disgust”</p>

<p><a href="/content/images/2026/04/mai2-07.png"><img src="/content/images/2026/04/mai2-07.png" alt="/content/images/2026/04/mai2-07.png" /></a></p>

<p>It worked fine, though I don’t really hear disgust in it</p>

<audio controls="" src="/content/images/2026/04/voice-1776256629459.mp3"></audio>

<p>I tried the same prompt with “Alder (American)” and “anger”</p>

<audio controls="" src="/content/images/2026/04/voice-1776256791349.mp3"></audio>

<h1 id="mai-2">MAI 2</h1>

<p>Let’s try some of those prompts form last time.. still blocked on even loosely copyrighted stuff</p>

<p><a href="/content/images/2026/04/mai2-13.png"><img src="/content/images/2026/04/mai2-13.png" alt="/content/images/2026/04/mai2-13.png" /></a></p>

<p>However, talking near the topic seemed to do better now:</p>

<p><a href="/content/images/2026/04/mai2-19.png"><img src="/content/images/2026/04/mai2-19.png" alt="/content/images/2026/04/mai2-19.png" /></a></p>

<p>Also, horror images are also blocked as before (trying the same prompt as last time)</p>

<p><a href="/content/images/2026/04/mai2-20.png"><img src="/content/images/2026/04/mai2-20.png" alt="/content/images/2026/04/mai2-20.png" /></a></p>

<p><strong>spoiler</strong> <em>see notes on AI Foundry later in article</em></p>

<p>Let’s try the “efficient” version with the prompt we did last time:</p>

<blockquote>
  <p>“In block letters on white background “Hello World” with a gentle artistic notes of grass and flower as if the letters were on a spring forest glen. light tones, watercolor”</p>
</blockquote>

<p>Here we see we now can get 4 responses which is pretty handy</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/mai2-14.mp4" type="video/mp4" />
</video>

<h2 id="diagrams">Diagrams</h2>

<p>so last time, the diagram ask was pretty bad:</p>

<p><a href="/content/images/2026/04/mai-07.png"><img src="/content/images/2026/04/mai-07.png" alt="/content/images/2026/04/mai-07.png" /></a></p>

<p>Let’s use the “Refined” option this time for one image that should be improved:</p>

<p><a href="/content/images/2026/04/mai2-15.png"><img src="/content/images/2026/04/mai2-15.png" alt="/content/images/2026/04/mai2-15.png" /></a></p>

<p>That is WAY better!</p>

<p><a href="/content/images/2026/04/mai2-16.png"><img src="/content/images/2026/04/mai2-16.png" alt="/content/images/2026/04/mai2-16.png" /></a></p>

<p>The “No Public Internet” line is a bit strange - I would likely remove that, and I would change “Private IP” to “Private IPs”, but otherwise that would be very usable in a deck.</p>

<h1 id="ai-foundry-deployment-with-custom-filter">AI Foundry deployment with custom filter</h1>

<p>As with Flux, let’s try deploying the new MAI model (2e) to our AI Foundry instances</p>

<p><a href="/content/images/2026/04/mai2-21.png"><img src="/content/images/2026/04/mai2-21.png" alt="/content/images/2026/04/mai2-21.png" /></a></p>

<p>I’ll then edit it and set the Content filter to the lowest settings</p>

<p><a href="/content/images/2026/04/mai2-22.png"><img src="/content/images/2026/04/mai2-22.png" alt="/content/images/2026/04/mai2-22.png" /></a></p>

<p>That actually worked</p>

<p><a href="/content/images/2026/04/mai2-23.png"><img src="/content/images/2026/04/mai2-23.png" alt="/content/images/2026/04/mai2-23.png" /></a></p>

<p>Granted, I’m now paying for these generations, but without (as many) nanny filters.</p>

<p>This is really important.  I have given feedback several times to Google that the blocking of knives meant for <em>real work</em> I do, where I need to see Medical setups, I cannot have Surgeons using plastic kitchen knives or diner utensils and they block anything sharp in most Gemini images.</p>

<p>Here I asked for: “Surgical room, patient prepped for surgery, modern operating room, table with surgeons utensils including scalpel and forceps”</p>

<p>I got a pretty reasonable response</p>

<p><a href="/content/images/2026/04/mai2-24.png"><img src="/content/images/2026/04/mai2-24.png" alt="/content/images/2026/04/mai2-24.png" /></a></p>

<p>I tried “fast” mode now in Gemini and it looks reasonable, but there are a LOT of clips there on the table</p>

<p><a href="/content/images/2026/04/mai2-25.png"><img src="/content/images/2026/04/mai2-25.png" alt="/content/images/2026/04/mai2-25.png" /></a></p>

<p>Even though I explicitly mentioned scalpel (same prompt), MJ wouldn’t give it to me</p>

<p><a href="/content/images/2026/04/mai2-26.png"><img src="/content/images/2026/04/mai2-26.png" alt="/content/images/2026/04/mai2-26.png" /></a></p>

<p>And just to test, I asked Gemini in “Thinking” mode to create the horror image and it seemed to do it okay now:</p>

<blockquote>
  <p>Photorealistic painting of a Victorian woman with the upper half of her head wrapped in bandages. The lower half of her face reveals a mouth full of monstrous teeth that are falling out. Numerous other teeth hang from above on threads. Red liquid stains around mouth. Insanely intricately detailed white fabric clothing decorated with teeth. Dramatic lighting. Esoteric, horror, creepy, unsettling, disorienting, dreamcore. Surreal</p>
</blockquote>

<p><a href="/content/images/2026/04/mai2-27.png"><img src="/content/images/2026/04/mai2-27.png" alt="/content/images/2026/04/mai2-27.png" /></a></p>

<p>My last quick test of MAI is to do the one that is always flagged for violence/BDSM: “Software developers in a meeting room tied up in chairs with ropes”.</p>

<p><a href="/content/images/2026/04/mai2-28.png"><img src="/content/images/2026/04/mai2-28.png" alt="/content/images/2026/04/mai2-28.png" /></a></p>

<p>That is awesome.  I might tweak the size of the room, but that was exactly what I was hoping for - the ropes around around the chairs, not loose, and some of them are gagged, which was a bit more than I wanted, but hey, it’s riffing on my ask.</p>

<h2 id="using-urls">Using URLS</h2>

<p>I was curious if MAI could pull from a URL.  I asked it:</p>

<blockquote>
  <p>show an office building with the logo from https://www.logo.wine/logo/Control_Data_Corporation</p>
</blockquote>

<p>It figured out the words, but not the logo</p>

<p><a href="/content/images/2026/04/mai2-30.png"><img src="/content/images/2026/04/mai2-30.png" alt="/content/images/2026/04/mai2-30.png" /></a></p>

<p><a href="/content/images/2026/04/mai2-33.png"><img src="/content/images/2026/04/mai2-33.png" alt="/content/images/2026/04/mai2-33.png" /></a></p>

<p>Google Gemini did succeed and funny enough gave it a retro look with 1970s cars</p>

<p><a href="/content/images/2026/04/mai2-31.png"><img src="/content/images/2026/04/mai2-31.png" alt="/content/images/2026/04/mai2-31.png" /></a></p>

<p><a href="/content/images/2026/04/mai2-32.png"><img src="/content/images/2026/04/mai2-32.png" alt="/content/images/2026/04/mai2-32.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>Well, I do think MAI has had some pretty impressive improvements in the last few weeks period. Last time I wrote an article, I really thumbed my nose at it and suggested it was very far behind period. But as we saw, the voice transcription is very good.</p>

<p>The voice creation, the text to voice is okay. And the image model has greatly improved, if for no other reason, diagrams are actually usable period. I think going forward, I will regularly check back on the MAI playground and see if it’s improved at all period. My final test here is using a Android app that’s a voice recorder to record this summary and transcribe it. We’ll see how it goes.</p>

<h2 id="addendum">Addendum</h2>

<p>I was experimenting with the AI Foundry deployment after I wrote that summary and in light of that, I do think MAI2 will be one of my go-tos when I’m nanny-blocked by MJ and Gemini.  I was especially impressed with the tied-up-developers image.</p>

<p>Also, in just the week I was away at Next26, I noticed UI improvements on the MAI page, the layout now getting a bit tighter:</p>

<p><a href="/content/images/2026/04/mai2-34.png"><img src="/content/images/2026/04/mai2-34.png" alt="/content/images/2026/04/mai2-34.png" /></a></p>

<p>compared to just a bit over a week ago:</p>

<p><a href="/content/images/2026/04/mai2-01.png"><img src="/content/images/2026/04/mai2-01.png" alt="/content/images/2026/04/mai2-01.png" /></a></p>

<p>So this is one to watch.</p>]]></content><author><name>Isaac Johnson</name></author><category term="GenAI" /><category term="MAI" /><category term="Gemini" /><summary type="html"><![CDATA[In my last MAI post we just focused on the MAI-Image-2 image generation model. It was basic but functional.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/mai2bg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/mai2bg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hugo and Serverless in GCP</title><link href="https://freshbrewed.science/2026/04/23/hugo3.html" rel="alternate" type="text/html" title="Hugo and Serverless in GCP" /><published>2026-04-23T10:00:01+00:00</published><updated>2026-04-23T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/23/hugo3</id><content type="html" xml:base="https://freshbrewed.science/2026/04/23/hugo3.html"><![CDATA[<p>In <a href="/2026/04/16/hugo.html">Part 1</a> we explored setting up a Hugo based blog in Azure using <a href="https://azure.microsoft.com/en-us/products/frontdoor">Azure Front Door</a> and <a href="https://azure.microsoft.com/en-us/products/storage/blobs/">Storage Buckets</a>.  Earlier this week we followed up with <a href="/2026/04/21/hugo2.html">Part 2</a> in GCP using <a href="https://cloud.google.com/load-balancing">Application Load Balancers</a> and Storage Buckets.</p>

<p>Because, in both cases, we have to have persistent running infrastructure, there is a cost that might still be a bit high for your small time blogger looking to get going on the cheap.</p>

<p>Today we’ll build off of the prior two posts to see if we can create a usable container that could serve our small blog.  Once we have it containerized, we can start to explore serverless options and compare costs.</p>

<h1 id="containerizing-hugo">Containerizing Hugo</h1>

<p>The workflow today will build and push the static “public” folder out to one of two buckets to share via GCP ALBs</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">$ cat .gitea/workflows/cicd.yaml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Gitea Actions Test</span>
<span class="na">run-name</span><span class="pi">:</span> <span class="s">$ is testing out Gitea Actions 🚀</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">Explore-Gitea-Actions</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">my_custom_label</span>
    <span class="na">container</span><span class="pi">:</span> <span class="s">node:22</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt update -y</span>
          <span class="s">umask 0002</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo zip</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">setup gcloud</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg</span>
          <span class="s">echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt-get update</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt-get install -y google-cloud-cli</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test gcloud</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">gcloud version</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check out repository code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">submodules</span><span class="pi">:</span> <span class="s">recursive</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip</span>
          <span class="s">wget https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_0.160.0_linux-amd64.tar.gz</span>
          <span class="s">tar -xzvf hugo_0.160.0_linux-amd64.tar.gz</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Hugo version..."</span>
          <span class="s">pwd</span>
          <span class="s">./hugo version</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">hugo build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">./hugo</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">create sa and auth</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">cat &lt;&lt;EOF &gt; /tmp/gcp-key.json</span>
          <span class="s">$GCP_SAJSON</span>
          <span class="s">EOF</span>
          <span class="s">gcloud auth activate-service-account --key-file=/tmp/gcp-key.json</span>
          <span class="s">gcloud config set project myanthosproject2</span>
          <span class="s"># export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-file.json"</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">GCP_SAJSON</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test bucket</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># test</span>
          <span class="s">gcloud storage buckets list gs://dbeelogsme</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Branch check and upload to GCS</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
                <span class="s">echo "✅ On main branch, proceeding with GCS sync..."</span>
                <span class="s"># -r is recursive, -d deletes files in destination not in source (optional)</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme --recursive</span>
            <span class="s">else</span>
                <span class="s">echo "⚠️ Not on main branch, uploading to testing path."</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme-test --recursive</span>
            <span class="s">fi</span>
</code></pre></div></div>

<p>I did some looking and there are two ways to attack this:</p>

<h2 id="turning-into-a-container">Turning into a Container</h2>

<p>we can build our site into a folder that is then served by a lightweight Nginx process.  Adding Minify will shrink our JS and CSS to load even faster:</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Stage 1: Build</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">hugomods/hugo:debian-dart-sass-0.160.1</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span>hugo <span class="nt">--minify</span>

<span class="c"># Stage 2: Serve</span>
<span class="k">FROM</span><span class="s"> nginx:alpine</span>
<span class="k">COPY</span><span class="s"> --from=builder /src/public /usr/share/nginx/html</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
</code></pre></div></div>

<p><em>Note: I avoid Nightly tags, but if you read this well after I publish, you may wish to lookup the <a href="https://hub.docker.com/r/hugomods/hugo/tags">latest tag</a> from Dockerhub</em></p>

<p>Let’s build and serve it up as it stands</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker build -t myhugo:0.1 .
[+] Building 57.4s (13/13) FINISHED                                                                           docker:default
 =&gt; [internal] load build definition from Dockerfile                                                                    0.0s
 =&gt; =&gt; transferring dockerfile: 251B                                                                                    0.0s
 =&gt; [internal] load metadata for docker.io/library/nginx:alpine                                                         0.0s
 =&gt; [internal] load metadata for docker.io/hugomods/hugo:debian-dart-sass-0.160.1                                       1.3s
 =&gt; [auth] hugomods/hugo:pull token for registry-1.docker.io                                                            0.0s
 =&gt; [internal] load .dockerignore                                                                                       0.0s
 =&gt; =&gt; transferring context: 2B                                                                                         0.0s
 =&gt; [builder 1/4] FROM docker.io/hugomods/hugo:debian-dart-sass-0.160.1@sha256:5dc92602efb1e34e0ea5ec0576b4af86cd984c  54.2s
 =&gt; =&gt; resolve docker.io/hugomods/hugo:debian-dart-sass-0.160.1@sha256:5dc92602efb1e34e0ea5ec0576b4af86cd984c56165b53c  0.0s
 =&gt; =&gt; sha256:da539b6761059a0a114c6671f1267b57445e3a54da023db5c28be019e40f0284 28.24MB / 28.24MB                        1.5s
 =&gt; =&gt; sha256:2a140fd0d6cdfea5acb1151c568cc81dc3167ca05f88d4c7b8f32d3701b0f59a 185.96MB / 185.96MB                     10.8s
 =&gt; =&gt; sha256:136c5a6247883b7423ee593ae6501d3ee6d7991a5d27575c56915892423d0eab 6.49kB / 6.49kB                          0.0s
 =&gt; =&gt; sha256:4effc8e7eba82bfc5792cbd602df13269dd525e5b6582158480b6ddf13ae8ca7 2.60MB / 2.60MB                          0.9s
 =&gt; =&gt; sha256:5dc92602efb1e34e0ea5ec0576b4af86cd984c56165b53c6979dfdcc99d7ce53 1.61kB / 1.61kB                          0.0s
 =&gt; =&gt; sha256:da2771f752bf97a58e1fd6c8bb7d67c812e2039b6809e2e6b6c352c04ab90ec0 4.47kB / 4.47kB                          0.0s
 =&gt; =&gt; sha256:365f1c3444f1f61c61f0f1bcd84024d70b5920a6055ba41346fdefff2353ae7e 10.13MB / 10.13MB                        1.7s
 =&gt; =&gt; sha256:3e2435c784152437773af364f87dd5bde2c5f98474cfed4cf34d66cd7695f2e6 44.91MB / 44.91MB                        4.9s
 =&gt; =&gt; extracting sha256:da539b6761059a0a114c6671f1267b57445e3a54da023db5c28be019e40f0284                               3.0s
 =&gt; =&gt; sha256:a80847ad56c3e290479f75b4f852a9d88a7c998ecc9ada2e1789844ba3aa6cd3 3.06MB / 3.06MB                          2.5s
 =&gt; =&gt; sha256:88c11d9c8b96394e9fda9d6d0c0789fe6cf2ea8813996f448503bfedcf058550 67.22MB / 67.22MB                        7.1s
 =&gt; =&gt; sha256:093f9586bc2c23b408ee3ed8d1119b6c3d6e7e1761ca84849ccd4405788cc19a 166B / 166B                              5.3s
 =&gt; =&gt; sha256:d785c4f6bdcd3f950f966e873c1a065584b3812e36e93c39df3dcb74c547d6d9 15.24MB / 15.24MB                        7.1s
 =&gt; =&gt; sha256:dd4d084397cffda26c36701bc49ea7215821af065fea13b3faac93e975bcaed6 15.20MB / 15.20MB                        8.1s
 =&gt; =&gt; sha256:a0db06d66d4ebe0e8db1e99b426e21fa2818328aa3f3145f6d9a8dbc4f2bb82c 162B / 162B                              7.3s
 =&gt; =&gt; sha256:95428f40ad6f7cdd5be90825809d3f2c795798376b2ec0a1e191e6d40fefbd89 4.54MB / 4.54MB                          8.0s
 =&gt; =&gt; sha256:a3f8b4b6e3c1bf86dd4b7a52a5fa0db9592fa5f1634c37b6e7d20c17634d54b8 1.05kB / 1.05kB                          8.3s
 =&gt; =&gt; sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 32B / 32B                                8.2s
 =&gt; =&gt; sha256:170c0ab98110540897a5937a18dc4a89a4660121fb8b44cc11572e6dd98733d8 44.06MB / 44.06MB                       10.4s
 =&gt; =&gt; sha256:5a2e3f8f48cf9bb4e2c66236ad6a526963719d38e32a9e2521c6a905ccafd3ce 535B / 535B                              8.5s
 =&gt; =&gt; sha256:0d77e7d623f7428915719c2ffeedad814a3cc4fd54b7ed25dc35a957f636b31f 3.31kB / 3.31kB                          8.9s
 =&gt; =&gt; sha256:7b3ec2c133af7076907cc55c323a47cdcde4e93a4ded75d2e357846ec85f8dd5 94B / 94B                                9.3s
 =&gt; =&gt; extracting sha256:2a140fd0d6cdfea5acb1151c568cc81dc3167ca05f88d4c7b8f32d3701b0f59a                              14.6s
 =&gt; =&gt; extracting sha256:4effc8e7eba82bfc5792cbd602df13269dd525e5b6582158480b6ddf13ae8ca7                               1.0s
 =&gt; =&gt; extracting sha256:365f1c3444f1f61c61f0f1bcd84024d70b5920a6055ba41346fdefff2353ae7e                               2.5s
 =&gt; =&gt; extracting sha256:3e2435c784152437773af364f87dd5bde2c5f98474cfed4cf34d66cd7695f2e6                               3.6s
 =&gt; =&gt; extracting sha256:a80847ad56c3e290479f75b4f852a9d88a7c998ecc9ada2e1789844ba3aa6cd3                               0.4s
 =&gt; =&gt; extracting sha256:88c11d9c8b96394e9fda9d6d0c0789fe6cf2ea8813996f448503bfedcf058550                              12.0s
 =&gt; =&gt; extracting sha256:093f9586bc2c23b408ee3ed8d1119b6c3d6e7e1761ca84849ccd4405788cc19a                               0.0s
 =&gt; =&gt; extracting sha256:d785c4f6bdcd3f950f966e873c1a065584b3812e36e93c39df3dcb74c547d6d9                               0.5s
 =&gt; =&gt; extracting sha256:dd4d084397cffda26c36701bc49ea7215821af065fea13b3faac93e975bcaed6                               2.2s
 =&gt; =&gt; extracting sha256:a0db06d66d4ebe0e8db1e99b426e21fa2818328aa3f3145f6d9a8dbc4f2bb82c                               0.0s
 =&gt; =&gt; extracting sha256:95428f40ad6f7cdd5be90825809d3f2c795798376b2ec0a1e191e6d40fefbd89                               0.3s
 =&gt; =&gt; extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1                               0.0s
 =&gt; =&gt; extracting sha256:a3f8b4b6e3c1bf86dd4b7a52a5fa0db9592fa5f1634c37b6e7d20c17634d54b8                               0.0s
 =&gt; =&gt; extracting sha256:170c0ab98110540897a5937a18dc4a89a4660121fb8b44cc11572e6dd98733d8                               2.2s
 =&gt; =&gt; extracting sha256:5a2e3f8f48cf9bb4e2c66236ad6a526963719d38e32a9e2521c6a905ccafd3ce                               0.0s
 =&gt; =&gt; extracting sha256:0d77e7d623f7428915719c2ffeedad814a3cc4fd54b7ed25dc35a957f636b31f                               0.0s
 =&gt; =&gt; extracting sha256:7b3ec2c133af7076907cc55c323a47cdcde4e93a4ded75d2e357846ec85f8dd5                               0.0s
 =&gt; [internal] load build context                                                                                       0.1s
 =&gt; =&gt; transferring context: 65.22kB                                                                                    0.1s
 =&gt; CACHED [stage-1 1/2] FROM docker.io/library/nginx:alpine                                                            0.0s
 =&gt; [builder 2/4] WORKDIR /src                                                                                          0.1s
 =&gt; [builder 3/4] COPY . .                                                                                              0.5s
 =&gt; [builder 4/4] RUN hugo --minify                                                                                     0.8s
 =&gt; [stage-1 2/2] COPY --from=builder /src/public /usr/share/nginx/html                                                 0.2s
 =&gt; exporting to image                                                                                                  0.2s
 =&gt; =&gt; exporting layers                                                                                                 0.2s
 =&gt; =&gt; writing image sha256:9535b1d16a40bc68b9a0cbfbb30ffdd10f298a907f6aa31de3029cdd608e30e7                            0.0s
 =&gt; =&gt; naming to docker.io/library/myhugo:0.1                                                                           0.0s

</code></pre></div></div>

<p>Just to avoid port conflicts, I’ll server this up locally on 8088</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ docker run -p 8088:80 myhugo:0.1
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/04/13 10:59:12 [notice] 1#1: using the "epoll" event method
2026/04/13 10:59:12 [notice] 1#1: nginx/1.29.4
2026/04/13 10:59:12 [notice] 1#1: built by gcc 15.2.0 (Alpine 15.2.0)
2026/04/13 10:59:12 [notice] 1#1: OS: Linux 6.6.87.2-microsoft-standard-WSL2
2026/04/13 10:59:12 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2026/04/13 10:59:12 [notice] 1#1: start worker processes
2026/04/13 10:59:12 [notice] 1#1: start worker process 30
2026/04/13 10:59:12 [notice] 1#1: start worker process 31
2026/04/13 10:59:12 [notice] 1#1: start worker process 32
2026/04/13 10:59:12 [notice] 1#1: start worker process 33
2026/04/13 10:59:12 [notice] 1#1: start worker process 34
2026/04/13 10:59:12 [notice] 1#1: start worker process 35
2026/04/13 10:59:12 [notice] 1#1: start worker process 36
2026/04/13 10:59:12 [notice] 1#1: start worker process 37
2026/04/13 10:59:12 [notice] 1#1: start worker process 38
2026/04/13 10:59:12 [notice] 1#1: start worker process 39
2026/04/13 10:59:12 [notice] 1#1: start worker process 40
2026/04/13 10:59:12 [notice] 1#1: start worker process 41
2026/04/13 10:59:12 [notice] 1#1: start worker process 42
2026/04/13 10:59:12 [notice] 1#1: start worker process 43
2026/04/13 10:59:12 [notice] 1#1: start worker process 44
2026/04/13 10:59:12 [notice] 1#1: start worker process 45
</code></pre></div></div>

<p>It looks good and is quite quick to respond</p>

<p><a href="/content/images/2026/04/hugoserverless-01.png"><img src="/content/images/2026/04/hugoserverless-01.png" alt="/content/images/2026/04/hugoserverless-01.png" /></a></p>

<p>The second way we can serve this up is with an active Hugo process</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> hugomods/hugo:debian-dart-sass-0.160.1</span>

<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">EXPOSE</span><span class="s"> 1313</span>

<span class="c"># Standard hugo server command</span>
<span class="k">CMD</span><span class="s"> ["server", "--bind", "0.0.0.0", "--buildDrafts"]</span>
</code></pre></div></div>

<p>I can build and serve that up</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ docker build -t myhugo:0.2 .
[+] Building 1.6s (7/7) FINISHED                                                                              docker:default
 =&gt; [internal] load build definition from Dockerfile                                                                    0.0s
 =&gt; =&gt; transferring dockerfile: 195B                                                                                    0.0s
 =&gt; [internal] load metadata for docker.io/hugomods/hugo:debian-dart-sass-0.160.1                                       0.5s
 =&gt; [auth] hugomods/hugo:pull token for registry-1.docker.io                                                            0.0s
 =&gt; [internal] load .dockerignore                                                                                       0.0s
 =&gt; =&gt; transferring context: 2B                                                                                         0.0s
 =&gt; [1/2] FROM docker.io/hugomods/hugo:debian-dart-sass-0.160.1@sha256:5dc92602efb1e34e0ea5ec0576b4af86cd984c56165b53c  0.0s
 =&gt; CACHED [2/2] WORKDIR /src                                                                                        0.0s
 =&gt; [3/3] COPY . .                                                                                                       0.0s
 =&gt; exporting to image                                                                                                  0.1s
 =&gt; =&gt; exporting layers                                                                                                 0.0s
 =&gt; =&gt; writing image sha256:6b853c2a177f9a7c362d8206a269d6fff91ebf70d043244817b69481267de991                            0.0s
 =&gt; =&gt; naming to docker.io/library/myhugo:0.2                                                                           0.0s
</code></pre></div></div>

<p>Now running as before (but this time using port 1313)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ docker run -p 8088:1313 myhugo:0.2
Watching for changes in /src/archetypes, /src/assets/{icons,img}, /src/content/{categories,page,post}, /src/themes/hugo-theme-stack/archetypes, /src/themes/hugo-theme-stack/assets/{icons,scss,ts}, /src/themes/hugo-theme-stack/data, /src/themes/hugo-theme-stack/i18n, /src/themes/hugo-theme-stack/layouts/{_markup,_partials,_shortcodes,page}
Watching for config changes in /src/config/_default, /src/themes/hugo-theme-stack/config/_default
Start building sites …
hugo v0.160.1-d6bc8165e62b29d7d70ede01ed01d0f88de327e6+extended linux/amd64 BuildDate=2026-04-08T14:02:42Z VendorInfo=hugomods

WARN  deprecated: .Site.Data was deprecated in Hugo v0.156.0 and will be removed in a future release. Use hugo.Data instead.
WARN  Taxonomy categories not found
WARN  Taxonomy tags not found

              │ EN │ ZH │ ZH - HANT … │ JA
──────────────┼────┼────┼─────────────┼────
 Pages        │ 29 │ 17 │          17 │ 17
 Paginator    │  0 │  0 │           0 │  0
 pages        │    │    │             │
 Non-page     │  5 │  0 │           0 │  0
 files        │    │    │             │
 Static files │  0 │  0 │           0 │  0
 Processed    │ 26 │  0 │           0 │  0
 images       │    │    │             │
 Aliases      │ 10 │  5 │           5 │  5
 Cleaned      │  0 │  0 │           0 │  0

Built in 340 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 0.0.0.0)
Press Ctrl+C to stop
</code></pre></div></div>

<p>Performance-wise it was the same.</p>

<p>I ran both in parallel</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ docker run -d -p 8088:1313 myhugo:0.2
aa65a79d9ad2d557dae907526f6a3764dcdb6fafee2cfff074fdf74d3eb7d513
builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ docker run -d -p 8089:80 myhugo:0.1
81c9e97a83cb4c2e22db47e6f70b03253d835128a214601f4dd0079530f9ab28
</code></pre></div></div>

<p>then looked to Docker desktop to see if I could determine which approach would be better (to the side I refreshed a few times different pages to generate some form of load)</p>

<p>The NGinx approach used practically no CPU and 13Mb of memory</p>

<p><a href="/content/images/2026/04/hugoserverless-02.png"><img src="/content/images/2026/04/hugoserverless-02.png" alt="/content/images/2026/04/hugoserverless-02.png" /></a></p>

<p>The Hugo-as-a-server approach used much more CPU (spiking to 0.5%) and memory (36Mb)</p>

<p><a href="/content/images/2026/04/hugoserverless-03.png"><img src="/content/images/2026/04/hugoserverless-03.png" alt="/content/images/2026/04/hugoserverless-03.png" /></a></p>

<p>This confirmed my hypothesis - NGinx will take less CPU and Memory and when we start to get larger, this will make a difference.</p>

<p>Moreover, the <em>size</em> is what will cost us in Container Registries.  The full size of the Hugo approach generates a 1.29Gb image presently and the Nginx as it is minified and just has the contents uses 83.2Mb.</p>

<p>From <code class="language-plaintext highlighter-rouge">docker ls</code></p>

<p><a href="/content/images/2026/04/hugoserverless-04.png"><img src="/content/images/2026/04/hugoserverless-04.png" alt="/content/images/2026/04/hugoserverless-04.png" /></a></p>

<h2 id="cicd">CICD</h2>

<p>When moving to a docker build approach, so much can be gutted out of our CICD as now Docker is really doing the work.</p>

<p>One of my patterns I often employ is to use the last line in a Dockerfile to tell my builds <em>where</em> to send this dockerfile.  This gives me more control on revisions (which is helpful later with helm)</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat Dockerfile
<span class="c"># Stage 1: Build</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">hugomods/hugo:debian-dart-sass-0.160.1</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span>hugo <span class="nt">--minify</span>

<span class="c"># Stage 2: Serve</span>
<span class="k">FROM</span><span class="s"> nginx:alpine</span>
<span class="k">COPY</span><span class="s"> --from=builder /src/public /usr/share/nginx/html</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="c">#harbor.freshbrewed.science/library/hugoblog:0.1</span>
</code></pre></div></div>

<p>While I do plan to start with my <a href="https://harbor.freshbrewed.science/">Harbor registry</a>, worry not, we’ll come back to GCP</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Hugo</span>
<span class="na">run-name</span><span class="pi">:</span> <span class="s">$ is building Hugo 🚀</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">Explore-Gitea-Actions</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">my_custom_label</span>
    <span class="na">container</span><span class="pi">:</span> <span class="s">node:22</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt update -y</span>
          <span class="s">umask 0002</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo zip</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">setup gcloud</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg</span>
          <span class="s">echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt-get update</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt-get install -y google-cloud-cli</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test gcloud</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">gcloud version</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check out repository code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">submodules</span><span class="pi">:</span> <span class="s">recursive</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build Dockerfile</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">whoami</span>
          <span class="s">which docker || true</span>
          <span class="s">apt update</span>
          <span class="s">cat /etc/os-release</span>
          <span class="s">apt install -y ca-certificates curl gnupg</span>
          <span class="s">mkdir -p /etc/apt/keyrings</span>
          <span class="s">curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg</span>
          <span class="s">echo \</span>
            <span class="s">"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \</span>
            <span class="s">focal stable" | tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</span>
          <span class="s">apt update</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin          </span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build Dockerfile</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"</span>
          <span class="s">docker build -t $BUILDIMGTAG .</span>
          <span class="s">docker images          </span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Tag and Push (Harbor)</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"</span>
          <span class="s">export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`"</span>
          <span class="s">docker tag $BUILDIMGTAG $FINALBUILDTAG</span>
          <span class="s">docker images</span>
          <span class="s">echo $CR_PAT | docker login harbor.freshbrewed.science -u $CR_USER --password-stdin</span>
          <span class="s">docker push $FINALBUILDTAG          </span>
        <span class="na">env</span><span class="pi">:</span> <span class="c1"># Or as an environment variable</span>
          <span class="na">CR_PAT</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">CR_USER</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Tag and Push (Dockerhub)</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"</span>
          <span class="s">docker tag $BUILDIMGTAG $DHUSER/$BUILDIMGTAG</span>
          <span class="s">docker images</span>
          <span class="s">echo $DHPAT | docker login -u $DHUSER --password-stdin</span>
          <span class="s">docker push $DHUSER/$BUILDIMGTAG          </span>
        <span class="na">env</span><span class="pi">:</span> <span class="c1"># Or as an environment variable</span>
          <span class="na">DHPAT</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">DHUSER</span><span class="pi">:</span> <span class="s">$</span>



      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip</span>
          <span class="s">wget https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_0.160.0_linux-amd64.tar.gz</span>
          <span class="s">tar -xzvf hugo_0.160.0_linux-amd64.tar.gz</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Hugo version..."</span>
          <span class="s">pwd</span>
          <span class="s">./hugo version</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">hugo build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">./hugo</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">create sa and auth</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">cat &lt;&lt;EOF &gt; /tmp/gcp-key.json</span>
          <span class="s">$GCP_SAJSON</span>
          <span class="s">EOF</span>
          <span class="s">gcloud auth activate-service-account --key-file=/tmp/gcp-key.json</span>
          <span class="s">gcloud config set project myanthosproject2</span>
          <span class="s"># export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-file.json"</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">GCP_SAJSON</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test bucket</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># test</span>
          <span class="s">gcloud storage buckets list gs://dbeelogsme</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Branch check and upload to GCS</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
                <span class="s">echo "✅ On main branch, proceeding with GCS sync..."</span>
                <span class="s"># -r is recursive, -d deletes files in destination not in source (optional)</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme --recursive</span>
            <span class="s">else</span>
                <span class="s">echo "⚠️ Not on main branch, uploading to testing path."</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme-test --recursive</span>
            <span class="s">fi</span>

</code></pre></div></div>

<p>This meant I needed to create secrets for Harbor as well as Dockerhub</p>

<p><a href="/content/images/2026/04/hugoserverless-05.png"><img src="/content/images/2026/04/hugoserverless-05.png" alt="/content/images/2026/04/hugoserverless-05.png" /></a></p>

<p>The flow ran without issue the first time (surprised as usually I make some kind of typo)</p>

<p><a href="/content/images/2026/04/hugoserverless-06.png"><img src="/content/images/2026/04/hugoserverless-06.png" alt="/content/images/2026/04/hugoserverless-06.png" /></a></p>

<p>I can now see my container in my Harbor</p>

<p><a href="/content/images/2026/04/hugoserverless-07.png"><img src="/content/images/2026/04/hugoserverless-07.png" alt="/content/images/2026/04/hugoserverless-07.png" /></a></p>

<p>And <a href="https://hub.docker.com/repository/docker/idjohnson/hugoblog/general">in Dockerhub</a></p>

<p><a href="/content/images/2026/04/hugoserverless-08.png"><img src="/content/images/2026/04/hugoserverless-08.png" alt="/content/images/2026/04/hugoserverless-08.png" /></a></p>

<h2 id="launching-in-cloud-run">Launching in Cloud Run</h2>

<p>From <a href="https://docs.cloud.google.com/run/docs/deploying#gcloud">the docs</a>, a deploy should be easy.  Though, presently there is a limit of max container sizes of 9.9Gb if using external Artifact Registries (so that just means if you get huge containers over time, you will need to use GAR)</p>

<p>I first went to fire it off from my Harbor</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ gcloud run deploy myhugo --image harbor.freshbrewed.science/library/hugobl
og@sha256:3aca1299b5751971032dd0e321494c976a008b491143ab58be7a63ce781fbdc6
Please specify a region:
 [1] africa-south1
 [2] asia-east1
 [3] asia-east2
 [4] asia-northeast1
 [5] asia-northeast2
 [6] asia-northeast3
 [7] asia-south1
 [8] asia-south2
 [9] asia-southeast1
 [10] asia-southeast2
 [11] asia-southeast3
 [12] australia-southeast1
 [13] australia-southeast2
 [14] europe-central2
 [15] europe-north1
 [16] europe-north2
 [17] europe-southwest1
 [18] europe-west1
 [19] europe-west10
 [20] europe-west12
 [21] europe-west2
 [22] europe-west3
 [23] europe-west4
 [24] europe-west6
 [25] europe-west8
 [26] europe-west9
 [27] me-central1
 [28] me-central2
 [29] me-west1
 [30] northamerica-northeast1
 [31] northamerica-northeast2
 [32] northamerica-south1
 [33] southamerica-east1
 [34] southamerica-west1
 [35] us-central1
 [36] us-east1
 [37] us-east4
 [38] us-east5
 [39] us-south1
 [40] us-west1
 [41] us-west2
 [42] us-west3
 [43] us-west4
 [44] cancel
Please enter numeric choice or text value (must exactly match list item):  35

To make this the default region, run `gcloud config set run/region us-central1`.

Allow unauthenticated invocations to [myhugo] (y/N)?  y

Deploying container to Cloud Run service [myhugo] in project [myanthosproject2] region [us-central1]
X Deploying new service...
  . Creating Revision...
  . Routing traffic...
  . Setting IAM Policy...
Deployment failed
ERROR: (gcloud.run.deploy) spec.template.spec.containers[0].image: Expected an image path like [host/]repo-path[:tag and/or @digest], where host is one of [region.]gcr.io, [region-]docker.pkg.dev or docker.io but obtained harbor.freshbrewed.science/library/hugoblog@sha256:3aca1299b5751971032dd0e321494c976a008b491143ab58be7a63ce781fbdc6. To deploy container images from other public or private registries, set up an Artifact Registry remote repository. See https://cloud.google.com/artifact-registry/docs/repositories/remote-repo.
</code></pre></div></div>

<p>But am reminded again by something that annoys me to no end - fixation on corporate SaaS providers.  Even though the docs suggest we can use an Artifact URL, the gcloud command is fixed to just “Dockerhub” and GCR/GAR.</p>

<p>We can actually build and run without having to leverage Dockerhub (I don’t want to be forced into a SaaS)</p>

<p>If we specify the region (or –source) it will do a “build and push” operation and create the GAR repository for us.</p>

<p>Later you will see why I want to do this (for test endpoints).  Still, booooo! on not letting me use Harbor</p>

<p><a href="/content/images/2026/04/princess-bride-boo.gif"><img src="/content/images/2026/04/princess-bride-boo.gif" alt="/content/images/2026/04/princess-bride-boo.gif" /></a></p>

<p>The build and push failed</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ gcloud run deploy myhugo --region us-central1
Deploying from source. To deploy a container use [--image]. See https://cloud.google.com/run/docs/deploying-source-code for more details.
Source code location (/home/builder/Workspaces/hugo-initech):
Next time, you can use `--source .` argument to deploy the current directory.

Deploying from source requires an Artifact Registry Docker repository to store built containers. A repository named
[cloud-run-source-deploy] in region [us-central1] will be created.

Do you want to continue (Y/n)?  y

Allow unauthenticated invocations to [myhugo] (y/N)?  y

Building using Dockerfile and deploying container to Cloud Run service [myhugo] in project [myanthosproject2] region [us-central1]
X Building and deploying new service... Building Container.
  ✓ Creating Container Repository...
  ✓ Uploading sources...
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds;region=us-central1/534a
  a7ee-bacf-4940-b851-3db47828ee74?project=511842454269].
  - Creating Revision...
  . Routing traffic...
  ✓ Setting IAM Policy...
Deployment failed
ERROR: (gcloud.run.deploy) The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable within the allocated timeout. This can happen when the container port is misconfigured or if the timeout is too short. The health check timeout can be extended. Logs for this revision might contain more information.

Logs URL: https://console.cloud.google.com/logs/viewer?project=myanthosproject2&amp;resource=cloud_run_revision/service_name/myhugo/revision_name/myhugo-00001-cjh&amp;advancedFilter=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22myhugo%22%0Aresource.labels.revision_name%3D%22myhugo-00001-cjh%22
For more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-start
</code></pre></div></div>

<p>Looking at the logs (and now more carefully at the error message) it would seem it’s trying to use port “8080” when clearly the dockerfile says “EXPOSE 80”</p>

<p><a href="/content/images/2026/04/hugoserverless-09.png"><img src="/content/images/2026/04/hugoserverless-09.png" alt="/content/images/2026/04/hugoserverless-09.png" /></a></p>

<p>I saw some indication I might be able to set port with <code class="language-plaintext highlighter-rouge">--port</code> so I tried that next</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ gcloud run deploy myhugo --region us-central1 --source . --port 80
Building using Dockerfile and deploying container to Cloud Run service [myhugo] in project [myanthosproject2] region [us-central1]
✓ Building and deploying... Done.
  ✓ Uploading sources...
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds;region=us-central1/1aaf
  72ee-fe16-43de-a780-b2cebbb05868?project=511842454269].
  ✓ Creating Revision...
  ✓ Routing traffic...
Done.
Service [myhugo] revision [myhugo-00003-z44] has been deployed and is serving 100 percent of traffic.
Service URL: https://myhugo-511842454269.us-central1.run.app
</code></pre></div></div>

<p>That looks great!</p>

<p><a href="/content/images/2026/04/hugoserverless-10.png"><img src="/content/images/2026/04/hugoserverless-10.png" alt="/content/images/2026/04/hugoserverless-10.png" /></a></p>

<p>In the Cloud Console for <a href="https://console.cloud.google.com/run">Cloud Run</a>, we can go to the Networking tab to see the Endpoints section.  I’ll click “Manage” on custom domains</p>

<p><a href="/content/images/2026/04/hugoserverless-11.png"><img src="/content/images/2026/04/hugoserverless-11.png" alt="/content/images/2026/04/hugoserverless-11.png" /></a></p>

<p>I have some very old serverless verified domains there, but none would really work.</p>

<p><a href="/content/images/2026/04/hugoserverless-12.png"><img src="/content/images/2026/04/hugoserverless-12.png" alt="/content/images/2026/04/hugoserverless-12.png" /></a></p>

<p>Let’s create a new one for the <code class="language-plaintext highlighter-rouge">steeped.cloud</code> domain I setup last time (but didn’t use)</p>

<p><a href="/content/images/2026/04/hugoserverless-13.png"><img src="/content/images/2026/04/hugoserverless-13.png" alt="/content/images/2026/04/hugoserverless-13.png" /></a></p>

<p>The process is pretty quick, I went to the linked search site</p>

<p><a href="/content/images/2026/04/hugoserverless-14.png"><img src="/content/images/2026/04/hugoserverless-14.png" alt="/content/images/2026/04/hugoserverless-14.png" /></a></p>

<p>There it told me the TXT record to apply</p>

<p><a href="/content/images/2026/04/hugoserverless-15.png"><img src="/content/images/2026/04/hugoserverless-15.png" alt="/content/images/2026/04/hugoserverless-15.png" /></a></p>

<p>Which I did</p>

<p><a href="/content/images/2026/04/hugoserverless-16.png"><img src="/content/images/2026/04/hugoserverless-16.png" alt="/content/images/2026/04/hugoserverless-16.png" /></a></p>

<p>Then just clicked Verify to verify it</p>

<p><a href="/content/images/2026/04/hugoserverless-17.png"><img src="/content/images/2026/04/hugoserverless-17.png" alt="/content/images/2026/04/hugoserverless-17.png" /></a></p>

<p>I can now come back and add a “www” endpoint on ‘steeped.cloud’ using the “Domain Mappings” on Cloud Run</p>

<p><a href="/content/images/2026/04/hugoserverless-18.png"><img src="/content/images/2026/04/hugoserverless-18.png" alt="/content/images/2026/04/hugoserverless-18.png" /></a></p>

<p>It will want me to create a CNAME now</p>

<p><a href="/content/images/2026/04/hugoserverless-19.png"><img src="/content/images/2026/04/hugoserverless-19.png" alt="/content/images/2026/04/hugoserverless-19.png" /></a></p>

<p>Which I did in Cloud DNS</p>

<p><a href="/content/images/2026/04/hugoserverless-20.png"><img src="/content/images/2026/04/hugoserverless-20.png" alt="/content/images/2026/04/hugoserverless-20.png" /></a></p>

<p>At least for me, it didn’t refresh right away - I had to click the “Refresh” button in the upper right</p>

<p><a href="/content/images/2026/04/hugoserverless-21.png"><img src="/content/images/2026/04/hugoserverless-21.png" alt="/content/images/2026/04/hugoserverless-21.png" /></a></p>

<p>The step says i need to configure certificates</p>

<p><a href="/content/images/2026/04/hugoserverless-22.png"><img src="/content/images/2026/04/hugoserverless-22.png" alt="/content/images/2026/04/hugoserverless-22.png" /></a></p>

<p>However, I have no idea where to do that or what it wants.  I was in the middle of searching and debugging and using thinking mode on Gemini to see if I missed something when the icon turned green.  So i just have to assume that it takes a while</p>

<p><a href="/content/images/2026/04/hugoserverless-23.png"><img src="/content/images/2026/04/hugoserverless-23.png" alt="/content/images/2026/04/hugoserverless-23.png" /></a></p>

<p>I can see the <code class="language-plaintext highlighter-rouge">run.app</code> URL without issue</p>

<p><a href="/content/images/2026/04/hugoserverless-24.png"><img src="/content/images/2026/04/hugoserverless-24.png" alt="/content/images/2026/04/hugoserverless-24.png" /></a></p>

<p>However, the custom domain fails to forward</p>

<p><a href="/content/images/2026/04/hugoserverless-25.png"><img src="/content/images/2026/04/hugoserverless-25.png" alt="/content/images/2026/04/hugoserverless-25.png" /></a></p>

<p>Again, I was deep into debugging when the pages just magically showed up - this was about 20 minutes after verifying the domain</p>

<p><a href="/content/images/2026/04/hugoserverless-26.png"><img src="/content/images/2026/04/hugoserverless-26.png" alt="/content/images/2026/04/hugoserverless-26.png" /></a></p>

<p>The pages now load fine in Firefox and Chrome</p>

<p><a href="/content/images/2026/04/hugoserverless-27.png"><img src="/content/images/2026/04/hugoserverless-27.png" alt="/content/images/2026/04/hugoserverless-27.png" /></a></p>

<h2 id="test-endpoint">Test endpoint</h2>

<p>I want to deploy now to a test endpoint, though as this would be just for me to verify contents, I have no need to add a DNS entry.</p>

<p>I’ll start with a local deployment - the reason is I do <strong>not</strong> want unauthenticated access - this is how I play to restrict access</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud run deploy myhugotest --region us-central1 --source . --port 80
Allow unauthenticated invocations to [myhugotest] (y/N)?  N

Building using Dockerfile and deploying container to Cloud Run service [myhugotest] in project [myanthosproject2] region [us-central1]
✓ Building and deploying new service... Done.
  ✓ Uploading sources...
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds;region=us-central1/afd3
  f6b7-178c-4043-bcf9-c1df6a3b55e7?project=511842454269].
  ✓ Creating Revision...
  ✓ Routing traffic...
Done.
Service [myhugotest] revision [myhugotest-00001-gfb] has been deployed and is serving 100 percent of traffic.
Service URL: https://myhugotest-511842454269.us-central1.run.app
</code></pre></div></div>

<p>If I view that URL now, I see we are forbidden</p>

<p><a href="/content/images/2026/04/hugoserverless-28.png"><img src="/content/images/2026/04/hugoserverless-28.png" alt="/content/images/2026/04/hugoserverless-28.png" /></a></p>

<p>This is because “Allow public access” is disabled</p>

<p><a href="/content/images/2026/04/hugoserverless-29.png"><img src="/content/images/2026/04/hugoserverless-29.png" alt="/content/images/2026/04/hugoserverless-29.png" /></a></p>

<p>To add a Principal via the Cloud Console, we can do that in Services with the Permissions link</p>

<p><a href="/content/images/2026/04/hugoserverless-30.png"><img src="/content/images/2026/04/hugoserverless-30.png" alt="/content/images/2026/04/hugoserverless-30.png" /></a></p>

<p>I’ll add myself as a cloud run invoker</p>

<p><a href="/content/images/2026/04/hugoserverless-31.png"><img src="/content/images/2026/04/hugoserverless-31.png" alt="/content/images/2026/04/hugoserverless-31.png" /></a></p>

<p>It may take a moment for the policy change to happen</p>

<p><a href="/content/images/2026/04/hugoserverless-32.png"><img src="/content/images/2026/04/hugoserverless-32.png" alt="/content/images/2026/04/hugoserverless-32.png" /></a></p>

<p>Even if I use gcloud to do the same thing</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud run services add-iam-policy-binding myhugotest --member="user:isaac.johnson@gmail.com" --role="roles/run.invoker" --region=us-central1
Updated IAM policy for service [myhugotest].
bindings:
- members:
  - user:isaac.johnson@gmail.com
  role: roles/run.invoker
etag: BwZPXkxAaKo=
version: 1
</code></pre></div></div>

<p>Accessing the URL https://myhugotest-511842454269.us-central1.run.app/ in my browser won’t help because the Cloud Run doesn’t know who I am.</p>

<p>I can use a <code class="language-plaintext highlighter-rouge">gcloud</code> command to determine this</p>

<p><a href="/content/images/2026/04/hugoserverless-33.png"><img src="/content/images/2026/04/hugoserverless-33.png" alt="/content/images/2026/04/hugoserverless-33.png" /></a></p>

<p>Or, I could use a Chrome extension like “ModHeader” to add the Bearer token to my request, then the page will load</p>

<p><a href="/content/images/2026/04/hugoserverless-34.png"><img src="/content/images/2026/04/hugoserverless-34.png" alt="/content/images/2026/04/hugoserverless-34.png" /></a></p>

<p>however, if you use ModHeader, remember to <em>remove</em> the value when done testing. I nearly had a panic attack when i stopped being able to see any GCP projects or tools</p>

<p><a href="/content/images/2026/04/hugoserverless-35.png"><img src="/content/images/2026/04/hugoserverless-35.png" alt="/content/images/2026/04/hugoserverless-35.png" /></a></p>

<p>But it was due to the Header re-write that ModHeader was still doing for the test blog.  Once i cleared the Authorization header, GCP Cloud Console came back</p>

<h3 id="adding-iap-proxy">Adding IAP Proxy</h3>

<p>Let’s go to the test service in Cloud Run and select “Identity Aware Proxy (IAP)” to add the IAP proxy and save</p>

<p><a href="/content/images/2026/04/hugoserverless-36.png"><img src="/content/images/2026/04/hugoserverless-36.png" alt="/content/images/2026/04/hugoserverless-36.png" /></a></p>

<p>I now see</p>

<p><a href="/content/images/2026/04/hugoserverless-37.png"><img src="/content/images/2026/04/hugoserverless-37.png" alt="/content/images/2026/04/hugoserverless-37.png" /></a></p>

<p>So I’ll add my user as an “IAP” httpsResourceAccessor.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud beta iap web add-iam-policy-binding     --resource-type
=cloud-run --service=myhugotest --region=us-central1 --member='user:isaac.johnson@gmail.com' --role='roles/iap.httpsResourceA
ccessor'
Updated IAM policy for cloud run [projects/511842454269/iap_web/cloud_run-us-central1/services/myhugotest].
</code></pre></div></div>

<p>After it activated (couple of minutes), I could now load the page - no Header modifications required</p>

<p><a href="/content/images/2026/04/hugoserverless-38.png"><img src="/content/images/2026/04/hugoserverless-38.png" alt="/content/images/2026/04/hugoserverless-38.png" /></a></p>

<p>If you wish to use the Cloud Console UI to add and remove users from the test endpoint you can with the “Edit policy” link</p>

<p><a href="/content/images/2026/04/hugoserverless-39.png"><img src="/content/images/2026/04/hugoserverless-39.png" alt="/content/images/2026/04/hugoserverless-39.png" /></a></p>

<h2 id="costs">Costs</h2>

<p>After I cleaned up, I saw a big jump in Compute Engine costs which surprised me:</p>

<p><a href="/content/images/2026/04/hugoserverless-43.png"><img src="/content/images/2026/04/hugoserverless-43.png" alt="/content/images/2026/04/hugoserverless-43.png" /></a></p>

<p>I switched to SKU view</p>

<p><a href="/content/images/2026/04/hugoserverless-44.png"><img src="/content/images/2026/04/hugoserverless-44.png" alt="/content/images/2026/04/hugoserverless-44.png" /></a></p>

<p>And realized it was due to Static IPs</p>

<p><a href="/content/images/2026/04/hugoserverless-45.png"><img src="/content/images/2026/04/hugoserverless-45.png" alt="/content/images/2026/04/hugoserverless-45.png" /></a></p>

<p>In GCP, Static IPs only get expensive if you <em>aren’t</em> actually using them.  If I did nothing, I would spend US$10 just on static IPs.</p>

<p>Going to <a href="https://console.cloud.google.com/networking/addresses/list">VPC Networks/IP addresses</a> I realized my mistake.</p>

<p>I had the IPs for the ALBs still there and from the icon, that clues me in to the fact they are not used</p>

<p><a href="/content/images/2026/04/hugoserverless-46.png"><img src="/content/images/2026/04/hugoserverless-46.png" alt="/content/images/2026/04/hugoserverless-46.png" /></a></p>

<p>You don’t really delete an address, just ‘release’ it</p>

<p><a href="/content/images/2026/04/hugoserverless-47.png"><img src="/content/images/2026/04/hugoserverless-47.png" alt="/content/images/2026/04/hugoserverless-47.png" /></a></p>

<p>I gave it a day and came back to review costs.</p>

<p><a href="/content/images/2026/04/hugoserverless-48.png"><img src="/content/images/2026/04/hugoserverless-48.png" alt="/content/images/2026/04/hugoserverless-48.png" /></a></p>

<p>We can see our daily rate has dropped considerably.  Even if we assume that Cloud DNS and Compute costs are roughly static per day (and they may capture operations or prior costs), we are looking at roughly US$0.0628 per day to run this blog WITH a protected test site.</p>

<p><a href="/content/images/2026/04/hugoserverless-50.png"><img src="/content/images/2026/04/hugoserverless-50.png" alt="/content/images/2026/04/hugoserverless-50.png" /></a></p>

<p>That means we would be just under US$2/mo for a very functional blog.  These are prices I really like.</p>

<h2 id="summary">Summary</h2>

<p>Today we tackled moving a site from Global Application Load Balancers and CDNs over to Cloud Run functions with custom domain mappings.</p>

<p>While ALBs are persistent global endpoints that properly serve traffic, due to their persistent nature, they also cost us between US$15 and $20 to keep running all the time.  Cloud Run, one of GCP’s serverless options, is a pay-on-demand situation.  If no one is visiting the website at that moment, we aren’t paying for the service.  This is excellent for entry level sites with sporadic or minimal traffic.</p>

<p>Additionally, the “test” endpoint setup we built with Identity Aware Proxies is a far superior option, in my opinion, to having to possibly manage CIDR ingress blocks in a Load Balancer restriction.</p>

<p>I did not have to build out an OAuth2 federated IdP flow in a container or app - Google is doing this for me, assumably for free.    It also doesn’t require Chrome.  I found Firefox worked just as well (and using an InPrivate window showed i needed to login)</p>

<p><a href="/content/images/2026/04/hugoserverless-40.png"><img src="/content/images/2026/04/hugoserverless-40.png" alt="/content/images/2026/04/hugoserverless-40.png" /></a></p>

<p>This includes the proper MFA one’s Google account likely has</p>

<p><a href="/content/images/2026/04/hugoserverless-41.png"><img src="/content/images/2026/04/hugoserverless-41.png" alt="/content/images/2026/04/hugoserverless-41.png" /></a></p>

<p><a href="/content/images/2026/04/hugoserverless-42.png"><img src="/content/images/2026/04/hugoserverless-42.png" alt="/content/images/2026/04/hugoserverless-42.png" /></a></p>

<p>Once I reviewed the updated costs just using Cloud Run, this became an utter no-brainer.  This is by far the cheapest option at small scales.</p>

<p>With a per vCPU charge of $0.000018 and per GiB-second $0.000002, auto-scaling set to the default 20 with limits of 1 vCPU and 512MiB, the max charge under sustained 100% usage would be $984.96/month.</p>

<p>To avoid some kind of DDOS, crazy traffic problem, I set the scaling max down to 1</p>

<p><a href="/content/images/2026/04/hugoserverless-51.png"><img src="/content/images/2026/04/hugoserverless-51.png" alt="/content/images/2026/04/hugoserverless-51.png" /></a></p>

<p>This means the max (again, for 100% all the time always on) would be $49.93 which wouldn’t be pleasant, but wouldn’t get me a spousaly thumping for a crazy high cloud bill.</p>

<p><strong>That said, consider budgets instead.</strong>  If you look back at <a href="https://freshbrewed.science/2024/09/19/gcpcosts.html">Cloud Spend: Find and Fix</a> or it’s prior article <a href="https://freshbrewed.science/2023/09/21/budgetsrevisit.html">Cloud Budgets and Alerts</a> I detail out how to use Cloud Budgets with tools like <a href="https://pagerduty.com/">PagerDuty</a> to get alerts if your spend jumps above a predefined limit.</p>

<p>I might allow up to 5 instances of this Cloud Run knowing I have budgets in place to alert me over US$20 - that way if some article catches fire and spikes traffic, users don’t get queued up, but when things are slow, I’m paying close to nothing.</p>]]></content><author><name>Isaac Johnson</name></author><category term="blog" /><category term="staticwebsite" /><category term="GCP" /><category term="Hugo" /><category term="markdown" /><category term="forgejo" /><category term="gitea" /><category term="cicd" /><category term="cloudrun" /><category term="serverless" /><summary type="html"><![CDATA[In Part 1 we explored setting up a Hugo based blog in Azure using Azure Front Door and Storage Buckets. Earlier this week we followed up with Part 2 in GCP using Application Load Balancers and Storage Buckets.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/hugoserverlessbg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/hugoserverlessbg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hugo and Static Sites in GCP</title><link href="https://freshbrewed.science/2026/04/21/hugo2.html" rel="alternate" type="text/html" title="Hugo and Static Sites in GCP" /><published>2026-04-21T10:00:01+00:00</published><updated>2026-04-21T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/21/hugo2</id><content type="html" xml:base="https://freshbrewed.science/2026/04/21/hugo2.html"><![CDATA[<p>In <a href="/2026/04/16/hugo.html">our last post</a> we explored <a href="https://gohugo.io/">Hugo</a> static sites with Azure, focusing on <a href="https://azure.microsoft.com/en-us/products/frontdoor">Azure Front Door</a> and <a href="https://azure.microsoft.com/en-us/products/storage/blobs/">Storage Buckets</a>.  Today, we will look at doing a similar thing in GCP with Hugo and <a href="https://cloud.google.com/cdn">Cloud CDN</a> and <a href="https://cloud.google.com/load-balancing">Application Load Balancers (ALBs)</a>.</p>

<p>The goal here is to compare process and costs and see how they differ.  Some of the findings may surprise you.</p>

<p>Let’s start with <a href="https://cloud.google.com/dns">Cloud DNS</a>…</p>

<h1 id="cloud-dns">Cloud DNS</h1>

<p>I have an unused domain in <a href="https://ionos.com/">Ionos</a> left over from a hackathon, dbeelogs.me</p>

<p><a href="/content/images/2026/04/clouddns-01.png"><img src="/content/images/2026/04/clouddns-01.png" alt="/content/images/2026/04/clouddns-01.png" /></a></p>

<p>Since I’m not 100% what the end costs might be, let’s do a trial run this with DNS before I register anything new.</p>

<p>Since I’m trying to do a GCP setup, let’s move that into Cloud DNS for management.</p>

<p><a href="/content/images/2026/04/clouddns-02.png"><img src="/content/images/2026/04/clouddns-02.png" alt="/content/images/2026/04/clouddns-02.png" /></a></p>

<p>I can then copy the Google DNS server entries over to the Name Servers area in IONOS</p>

<p><a href="/content/images/2026/04/clouddns-03.png"><img src="/content/images/2026/04/clouddns-03.png" alt="/content/images/2026/04/clouddns-03.png" /></a></p>

<p>I’m warned this can take 48 hours, but often I find it goes much quicker</p>

<p><a href="/content/images/2026/04/clouddns-04.png"><img src="/content/images/2026/04/clouddns-04.png" alt="/content/images/2026/04/clouddns-04.png" /></a></p>

<h1 id="buckets">Buckets</h1>

<p>Let’s create a bucket for our site</p>

<p><a href="/content/images/2026/04/hugoingcp-05.png"><img src="/content/images/2026/04/hugoingcp-05.png" alt="/content/images/2026/04/hugoingcp-05.png" /></a></p>

<p>There isn’t really cost savings between zone and region (both are about 2c/gb/month) so I’ll pick a region near me.</p>

<p><a href="/content/images/2026/04/hugoingcp-06.png"><img src="/content/images/2026/04/hugoingcp-06.png" alt="/content/images/2026/04/hugoingcp-06.png" /></a></p>

<p>While I am going with Standard for now, for a real blog that would last, “Autoclass” would save money over a long period</p>

<p><a href="/content/images/2026/04/hugoingcp-07.png"><img src="/content/images/2026/04/hugoingcp-07.png" alt="/content/images/2026/04/hugoingcp-07.png" /></a></p>

<p>You need to disable/uncheck the ‘enforce public access prevention’.  I think the words there could be better but having this box selected means you <em>cant</em> use it for website hosting</p>

<p><a href="/content/images/2026/04/hugoingcp-08.png"><img src="/content/images/2026/04/hugoingcp-08.png" alt="/content/images/2026/04/hugoingcp-08.png" /></a></p>

<p>I tend to like Soft delete for data protection.  I only use object versioning when storing immutable artifacts</p>

<p><a href="/content/images/2026/04/hugoingcp-09.png"><img src="/content/images/2026/04/hugoingcp-09.png" alt="/content/images/2026/04/hugoingcp-09.png" /></a></p>

<p>We now have a bucket created we can use with the blog</p>

<p><a href="/content/images/2026/04/hugoingcp-10.png"><img src="/content/images/2026/04/hugoingcp-10.png" alt="/content/images/2026/04/hugoingcp-10.png" /></a></p>

<p>I also made a “test” bucket we’ll use later</p>

<p><a href="/content/images/2026/04/hugoingcp-14.png"><img src="/content/images/2026/04/hugoingcp-14.png" alt="/content/images/2026/04/hugoingcp-14.png" /></a></p>

<h1 id="forgejo-cicd">Forgejo CICD</h1>

<p>At the conclusion of our last post, I had the Workflow set to upload to Azure using an SP</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Gitea Actions Test</span>
<span class="na">run-name</span><span class="pi">:</span> <span class="s">$ is testing out Gitea Actions 🚀</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">Explore-Gitea-Actions</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">my_custom_label</span>
    <span class="na">container</span><span class="pi">:</span> <span class="s">node:22</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt update -y</span>
          <span class="s">umask 0002</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo zip</span>
          <span class="s"># Install MS Key</span>
          
          <span class="s"># Use the official Microsoft script to handle repo mapping automatically</span>
          <span class="s">curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Azure CLI version..."</span>
          <span class="s">az --version</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check out repository code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">submodules</span><span class="pi">:</span> <span class="s">recursive</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip</span>
          <span class="s">wget https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_0.160.0_linux-amd64.tar.gz</span>
          <span class="s">tar -xzvf hugo_0.160.0_linux-amd64.tar.gz</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Hugo version..."</span>
          <span class="s">pwd</span>
          <span class="s">./hugo version</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">./hugo</span>
          <span class="s">zip -r public.zip public</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Branch check and upload</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
            <span class="s">echo "✅ On main branch, proceeding with Azure Blob upload..."</span>
            <span class="s">az storage blob upload-batch --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY -d '$web' -s ./public --overwrite</span>
          <span class="s">else</span>
            <span class="s">echo "⚠️ Not on main branch, uploading to testing container."</span>
            <span class="s">az storage blob upload --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY --container-name testing --name public-$GITHUB_RUN_NUMBER.zip --file ./public.zip --overwrite</span>
          <span class="s">fi  </span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">AZSTORAGE_ACCOUNT</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">AZSTORAGE_KEY</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Front Door cache purge</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
            <span class="s">az login --service-principal -u $ -p $ --tenant $</span>
            <span class="s">az afd endpoint purge \</span>
                <span class="s">--subscription $ \</span>
                <span class="s">--resource-group bloggingTestRG \</span>
                <span class="s">--profile-name ttpklat \</span>
                <span class="s">--endpoint-name tpklat \</span>
                <span class="s">--domains tpk.lat \</span>
                <span class="s">--content-paths '/*'</span>
          <span class="s">else</span>
            <span class="s">echo "⚠️ Not on main branch, skipping Azure Front Door purge."</span>
          <span class="s">fi</span>
</code></pre></div></div>

<p>We need to create a service account in GCP for this work.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud iam service-accounts create forgejo-publisher \
    --description="CI/CD publisher for Forgejo" \
    --display-name="Forgejo Publisher"

Created service account [forgejo-publisher]
</code></pre></div></div>

<p>By default, the SA has no powers, so I need to grant it bucket access</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud storage buckets add-iam-policy-binding gs://dbeelogsme \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/storage.objectAdmin"
    
bindings:
- members:
  - projectEditor:myanthosproject2
  - projectOwner:myanthosproject2
  role: roles/storage.legacyBucketOwner
- members:
  - projectViewer:myanthosproject2
  role: roles/storage.legacyBucketReader
- members:
  - projectEditor:myanthosproject2
  - projectOwner:myanthosproject2
  role: roles/storage.legacyObjectOwner
- members:
  - projectViewer:myanthosproject2
  role: roles/storage.legacyObjectReader
- members:
  - serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com
  role: roles/storage.objectAdmin
etag: CAI=
kind: storage#policy
resourceId: projects/_/buckets/dbeelogsme
version: 1
</code></pre></div></div>

<p>Also grant access to the test bucket</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud storage buckets add-iam-policy-binding gs://dbeelogsme-test \
    --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com" \
    --role="roles/storage.objectAdmin"
bindings:
- members:
  - projectEditor:myanthosproject2
  - projectOwner:myanthosproject2
  role: roles/storage.legacyBucketOwner
- members:
  - projectViewer:myanthosproject2
  role: roles/storage.legacyBucketReader
- members:
  - projectEditor:myanthosproject2
  - projectOwner:myanthosproject2
  role: roles/storage.legacyObjectOwner
- members:
  - projectViewer:myanthosproject2
  role: roles/storage.legacyObjectReader
- members:
  - serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com
  role: roles/storage.objectAdmin
etag: CAI=
kind: storage#policy
resourceId: projects/_/buckets/dbeelogsme-test
version: 1
</code></pre></div></div>

<p>When I was testing, I found the ‘objectAdmin’ missed some permissions so I went back and added ‘storage.admin’:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud storage buckets add-iam-policy-binding gs://dbeelogsme-test     --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com"     --role="roles/storage.admin"
$ gcloud storage buckets add-iam-policy-binding gs://dbeelogsme     --member="serviceAccount:forgejo-publisher@myanthosproject2.iam.gserviceaccount.com"     --role="roles/storage.admin"
</code></pre></div></div>

<p>If you forgot or don’t know the email address of that account, you can look it up in <a href="https://console.cloud.google.com/iam-admin/serviceaccounts">Service Accounts in “IAM &amp; Admin”</a> in the GCP console</p>

<p><a href="/content/images/2026/04/hugoingcp-11.png"><img src="/content/images/2026/04/hugoingcp-11.png" alt="/content/images/2026/04/hugoingcp-11.png" /></a></p>

<p>With Azure, we needed a Client ID, Client Secret and Tenant.  With GCP, we need a larger SA JSON file that includes a bit more (including certs)</p>

<p>We’ll create that SA JSON with</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcloud iam service-accounts keys create sa-key.json \
    --iam-account=forgejo-publisher@myanthosproject2.iam.gserviceaccount.com

created key [5f40xxxxxxxxxxxxxxxxxxxxxb2b] of type [json] as [sa-key.json] for [forgejo-publisher@myanthosproject2.iam.gserviceaccount.com]
</code></pre></div></div>

<p>which is now saved locally</p>

<p><a href="/content/images/2026/04/hugoingcp-12.png"><img src="/content/images/2026/04/hugoingcp-12.png" alt="/content/images/2026/04/hugoingcp-12.png" /></a></p>

<p>I’ll add that to the Hugo Blog Actions Secrets as “GCP_SAJSON”</p>

<p><a href="/content/images/2026/04/hugoingcp-13.png"><img src="/content/images/2026/04/hugoingcp-13.png" alt="/content/images/2026/04/hugoingcp-13.png" /></a></p>

<p>You might be able to use some pre-baked actions like</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: google-github-actions/auth@v2
        with:
          credentials_json: '$'

      - name: 'Set up Cloud SDK'
        uses: google-github-actions/setup-gcloud@v2
</code></pre></div></div>

<p>But I always prefer command line first.</p>

<p>I have a CDN step defined, but i won’t work for now as we haven’t setup Cloud CDN yet:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Gitea Actions Test</span>
<span class="na">run-name</span><span class="pi">:</span> <span class="s">$ is testing out Gitea Actions 🚀</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">Explore-Gitea-Actions</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">my_custom_label</span>
    <span class="na">container</span><span class="pi">:</span> <span class="s">node:22</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt update -y</span>
          <span class="s">umask 0002</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo zip</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">setup gcloud</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg</span>
          <span class="s">echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt-get update</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt-get install -y google-cloud-cli</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test gcloud</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">gcloud version</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check out repository code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">submodules</span><span class="pi">:</span> <span class="s">recursive</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip</span>
          <span class="s">wget https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_0.160.0_linux-amd64.tar.gz</span>
          <span class="s">tar -xzvf hugo_0.160.0_linux-amd64.tar.gz</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Hugo version..."</span>
          <span class="s">pwd</span>
          <span class="s">./hugo version</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">hugo build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">./hugo</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">create sa and auth</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">cat &lt;&lt;EOF &gt; /tmp/gcp-key.json</span>
          <span class="s">$GCP_SAJSON</span>
          <span class="s">EOF</span>
          <span class="s">gcloud auth activate-service-account --key-file=/tmp/gcp-key.json</span>
          <span class="s">gcloud config set project myanthosproject2</span>
          <span class="s"># export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-file.json"</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">GCP_SAJSON</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">test bucket</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># test</span>
          <span class="s">gcloud storage buckets list gs://dbeelogsme</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Branch check and upload to GCS</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
                <span class="s">echo "✅ On main branch, proceeding with GCS sync..."</span>
                <span class="s"># -r is recursive, -d deletes files in destination not in source (optional)</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme --recursive</span>
            <span class="s">else</span>
                <span class="s">echo "⚠️ Not on main branch, uploading to testing path."</span>
                <span class="s">gcloud storage cp ./public gs://dbeelogsme-test --recursive</span>
            <span class="s">fi</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Cloud CDN Cache Invalidation</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
                <span class="s">echo "🧹 Invalidating Cloud CDN cache..."</span>
                <span class="s"># Replace [URL_MAP_NAME] with your Load Balancer's URL map name</span>
                <span class="s">gcloud compute url-maps invalidate-cdn-cache [URL_MAP_NAME] --path "/*" --async</span>
            <span class="s">else</span>
                <span class="s">echo "⚠️ Skipping CDN invalidation."</span>
            <span class="s">fi</span>

</code></pre></div></div>

<p>It worked as far as I expected</p>

<p><a href="/content/images/2026/04/hugoingcp-15.png"><img src="/content/images/2026/04/hugoingcp-15.png" alt="/content/images/2026/04/hugoingcp-15.png" /></a></p>

<p>Since I was pushing directly to main, indeed I saw the files in the “production” bucket</p>

<p><a href="/content/images/2026/04/hugoingcp-16.png"><img src="/content/images/2026/04/hugoingcp-16.png" alt="/content/images/2026/04/hugoingcp-16.png" /></a></p>

<h1 id="cloud-cdn">Cloud CDN</h1>

<p><em>I went back and forth with Cloud CDN as my start point and Application Load Balancers (ALBs) as my start point.  If you want to just see the approach I landed on, skip ahead to “Challenges”</em></p>

<p>Let’s create a new Cloud CDN with a backend bucket</p>

<p><a href="/content/images/2026/04/hugoingcp-30.png"><img src="/content/images/2026/04/hugoingcp-30.png" alt="/content/images/2026/04/hugoingcp-30.png" /></a></p>

<p>I’ll create a new LB</p>

<p><a href="/content/images/2026/04/hugoingcp-31.png"><img src="/content/images/2026/04/hugoingcp-31.png" alt="/content/images/2026/04/hugoingcp-31.png" /></a></p>

<p>I’ll leave the default Cache settings</p>

<p><a href="/content/images/2026/04/hugoingcp-32.png"><img src="/content/images/2026/04/hugoingcp-32.png" alt="/content/images/2026/04/hugoingcp-32.png" /></a></p>

<p>Soon I saw the CDN Created.</p>

<p><a href="/content/images/2026/04/hugoingcp-33.png"><img src="/content/images/2026/04/hugoingcp-33.png" alt="/content/images/2026/04/hugoingcp-33.png" /></a></p>

<h2 id="modifying-the-lb-for-httpstls">Modifying the LB for HTTPS/TLS</h2>

<p>We now see the created prod load balancer</p>

<p><a href="/content/images/2026/04/hugoingcp-34.png"><img src="/content/images/2026/04/hugoingcp-34.png" alt="/content/images/2026/04/hugoingcp-34.png" /></a></p>

<p>I’ll now edit and add a frontend IP</p>

<p><a href="/content/images/2026/04/hugoingcp-35.png"><img src="/content/images/2026/04/hugoingcp-35.png" alt="/content/images/2026/04/hugoingcp-35.png" /></a></p>

<p>I’ll now set it to be HTTPS, use classic certs and click “Create new certificate”</p>

<p><a href="/content/images/2026/04/hugoingcp-36.png"><img src="/content/images/2026/04/hugoingcp-36.png" alt="/content/images/2026/04/hugoingcp-36.png" /></a></p>

<p>I’ll add the domain</p>

<p><a href="/content/images/2026/04/hugoingcp-37.png"><img src="/content/images/2026/04/hugoingcp-37.png" alt="/content/images/2026/04/hugoingcp-37.png" /></a></p>

<p>The SSL Cert was satisfied right away</p>

<p><a href="/content/images/2026/04/hugoingcp-38.png"><img src="/content/images/2026/04/hugoingcp-38.png" alt="/content/images/2026/04/hugoingcp-38.png" /></a></p>

<p>I think this was satisfied immediately because the first time through I did DNS auth with an ALB</p>

<h3 id="prior-steps-">Prior Steps …</h3>

<p><em>…I click “Create DNS Authorization”</em></p>

<p><a href="/content/images/2026/04/hugoingcp-26.png"><img src="/content/images/2026/04/hugoingcp-26.png" alt="/content/images/2026/04/hugoingcp-26.png" /></a></p>

<p><em>…I then see</em></p>

<p><a href="/content/images/2026/04/hugoingcp-27.png"><img src="/content/images/2026/04/hugoingcp-27.png" alt="/content/images/2026/04/hugoingcp-27.png" /></a></p>

<p><em>…It doesn’t realize my DNS is _in_ GCP so I’ll need to pop open another window and add that requested CName so LE (ACME) will satisfy the DNS challenge</em></p>

<p><a href="/content/images/2026/04/hugoingcp-28.png"><img src="/content/images/2026/04/hugoingcp-28.png" alt="/content/images/2026/04/hugoingcp-28.png" /></a></p>

<h2 id="challenges">Challenges…</h2>

<p>I fought this for a while.  Starting with the CDN then modifying the LB.  Or starting with the LB and trying to add a CDN.</p>

<p>I registered new domains.  Nothing seemed to get the cert side to work.  I always got a “FAILED_NOT_VISIBLE” error in certs</p>

<p><a href="/content/images/2026/04/hugoingcp-41.png"><img src="/content/images/2026/04/hugoingcp-41.png" alt="/content/images/2026/04/hugoingcp-41.png" /></a></p>

<p>And always a “DNS_PROBE_FINISHED_NXDOMAIN” with “This site can’t be reached”</p>

<p><a href="/content/images/2026/04/hugoingcp-42.png"><img src="/content/images/2026/04/hugoingcp-42.png" alt="/content/images/2026/04/hugoingcp-42.png" /></a></p>

<p>I decided to clean everything up - reserved IPs, CDNs and LBs to start over</p>

<p><a href="/content/images/2026/04/hugoingcp-43.png"><img src="/content/images/2026/04/hugoingcp-43.png" alt="/content/images/2026/04/hugoingcp-43.png" /></a></p>

<p>That meant removing my <a href="https://console.cloud.google.com/net-services/cdn/list">CDNs</a>, <a href="https://console.cloud.google.com/security/ccm/list/certificates">Certs</a>, and <a href="https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers">LoadBalancers</a>, but noting not the buckets</p>

<p><a href="/content/images/2026/04/hugoingcp-44.png"><img src="/content/images/2026/04/hugoingcp-44.png" alt="/content/images/2026/04/hugoingcp-44.png" /></a></p>

<p>And lastly any <a href="https://console.cloud.google.com/networking/addresses/list">remaining static IP addresses</a> not in use:</p>

<p><a href="/content/images/2026/04/hugoingcp-47.png"><img src="/content/images/2026/04/hugoingcp-47.png" alt="/content/images/2026/04/hugoingcp-47.png" /></a></p>

<h2 id="do-it-again">do it again..</h2>

<p>First, I went to the “Permissions” tab on my bucket, and in the “Permissions” pane, added “allUsers” to have storage object viewer - this would have prevented it from serving to unauthed users</p>

<p><a href="/content/images/2026/04/hugoingcp-45.png"><img src="/content/images/2026/04/hugoingcp-45.png" alt="/content/images/2026/04/hugoingcp-45.png" /></a></p>

<p><em>which was made clear in the confirmation dialogue</em></p>

<p><a href="/content/images/2026/04/hugoingcp-46.png"><img src="/content/images/2026/04/hugoingcp-46.png" alt="/content/images/2026/04/hugoingcp-46.png" /></a></p>

<p>Next, I’ll <a href="https://console.cloud.google.com/networking/addresses/add">get an IP address</a>.  I believe this was my problem all the other times - not getting a reserved IPv4 then setting up an A or CNAME record.  Here I named it <code class="language-plaintext highlighter-rouge">dbeelogsmelbstaticip</code>:</p>

<p><a href="/content/images/2026/04/hugoingcp-48.png"><img src="/content/images/2026/04/hugoingcp-48.png" alt="/content/images/2026/04/hugoingcp-48.png" /></a></p>

<p>We can now use that address</p>

<p><a href="/content/images/2026/04/hugoingcp-49.png"><img src="/content/images/2026/04/hugoingcp-49.png" alt="/content/images/2026/04/hugoingcp-49.png" /></a></p>

<p>To create an A record.  Here I am setting “www” to point to <code class="language-plaintext highlighter-rouge">34.111.157.82</code></p>

<p><a href="/content/images/2026/04/hugoingcp-50.png"><img src="/content/images/2026/04/hugoingcp-50.png" alt="/content/images/2026/04/hugoingcp-50.png" /></a></p>

<h3 id="-back-to-our-lb-edit">… Back to our LB edit</h3>

<p>Lastly, I just click “Update” to finish this work</p>

<p><a href="/content/images/2026/04/hugoingcp-39.png"><img src="/content/images/2026/04/hugoingcp-39.png" alt="/content/images/2026/04/hugoingcp-39.png" /></a></p>

<p>IF you want to have HTTP to HTTPS redirect, then you’ll need to reserve a static IP</p>

<p><a href="/content/images/2026/04/hugoingcp-40.png"><img src="/content/images/2026/04/hugoingcp-40.png" alt="/content/images/2026/04/hugoingcp-40.png" /></a></p>

<p>I’ll start by creating a new ALB</p>

<p><a href="/content/images/2026/04/hugoingcp-17.png"><img src="/content/images/2026/04/hugoingcp-17.png" alt="/content/images/2026/04/hugoingcp-17.png" /></a></p>

<p>It will be public facing</p>

<p><a href="/content/images/2026/04/hugoingcp-18.png"><img src="/content/images/2026/04/hugoingcp-18.png" alt="/content/images/2026/04/hugoingcp-18.png" /></a></p>

<p>I’ll make it global</p>

<p><a href="/content/images/2026/04/hugoingcp-51.png"><img src="/content/images/2026/04/hugoingcp-51.png" alt="/content/images/2026/04/hugoingcp-51.png" /></a></p>

<p>And the default global type (not classic)</p>

<p><a href="/content/images/2026/04/hugoingcp-52.png"><img src="/content/images/2026/04/hugoingcp-52.png" alt="/content/images/2026/04/hugoingcp-52.png" /></a></p>

<h3 id="lb-setup---frontend">LB Setup - Frontend</h3>

<p>This time we’ll pick HTTPS and the IP address we already created</p>

<p><a href="/content/images/2026/04/hugoingcp-53.png"><img src="/content/images/2026/04/hugoingcp-53.png" alt="/content/images/2026/04/hugoingcp-53.png" /></a></p>

<p>Then create new certificate</p>

<p><a href="/content/images/2026/04/hugoingcp-54.png"><img src="/content/images/2026/04/hugoingcp-54.png" alt="/content/images/2026/04/hugoingcp-54.png" /></a></p>

<p>and give it the same DNS name we used</p>

<p><a href="/content/images/2026/04/hugoingcp-55.png"><img src="/content/images/2026/04/hugoingcp-55.png" alt="/content/images/2026/04/hugoingcp-55.png" /></a></p>

<p>I enabled HTTP to HTTPS redirect</p>

<p><a href="/content/images/2026/04/hugoingcp-56.png"><img src="/content/images/2026/04/hugoingcp-56.png" alt="/content/images/2026/04/hugoingcp-56.png" /></a></p>

<p>Because I did a Global Application Balancer, I can now pick a Bucket for a backend (prior, when using regional, I could only see VMs, Cloud Run, GKE etc, but not buckets)</p>

<p><a href="/content/images/2026/04/hugoingcp-57.png"><img src="/content/images/2026/04/hugoingcp-57.png" alt="/content/images/2026/04/hugoingcp-57.png" /></a></p>

<p>Lastly, I just click create to complete the LB setup</p>

<p><a href="/content/images/2026/04/hugoingcp-58.png"><img src="/content/images/2026/04/hugoingcp-58.png" alt="/content/images/2026/04/hugoingcp-58.png" /></a></p>

<p>I now have a primary and redirect ALB</p>

<p><a href="/content/images/2026/04/hugoingcp-59.png"><img src="/content/images/2026/04/hugoingcp-59.png" alt="/content/images/2026/04/hugoingcp-59.png" /></a></p>

<p>The root seems to be serving <code class="language-plaintext highlighter-rouge">index.xml</code> instead of HTML</p>

<p><a href="/content/images/2026/04/hugoingcp-60.png"><img src="/content/images/2026/04/hugoingcp-60.png" alt="/content/images/2026/04/hugoingcp-60.png" /></a></p>

<p>However, if I type <code class="language-plaintext highlighter-rouge">index.html</code> the blog is served up</p>

<p><a href="/content/images/2026/04/hugoingcp-61.png"><img src="/content/images/2026/04/hugoingcp-61.png" alt="/content/images/2026/04/hugoingcp-61.png" /></a></p>

<p>The menu for that is kind of hidden (IMHO).  Go to the main buckets list page and find the “more actions” ellipse menu</p>

<p><a href="/content/images/2026/04/hugoingcp-62.png"><img src="/content/images/2026/04/hugoingcp-62.png" alt="/content/images/2026/04/hugoingcp-62.png" /></a></p>

<p>Chose “Edit Website Configuration”</p>

<p><a href="/content/images/2026/04/hugoingcp-63.png"><img src="/content/images/2026/04/hugoingcp-63.png" alt="/content/images/2026/04/hugoingcp-63.png" /></a></p>

<p>Now we can set the index and 404 pages (you want to at least set the index/home page)</p>

<p><a href="/content/images/2026/04/hugoingcp-64.png"><img src="/content/images/2026/04/hugoingcp-64.png" alt="/content/images/2026/04/hugoingcp-64.png" /></a></p>

<p>I can now use the URL without adding “index.html” and see the blog</p>

<p><a href="/content/images/2026/04/hugoingcp-65.png"><img src="/content/images/2026/04/hugoingcp-65.png" alt="/content/images/2026/04/hugoingcp-65.png" /></a></p>

<h2 id="adding-the-test-site">Adding the Test site</h2>

<p>I’ll now make a branch and a local post.  I can use <code class="language-plaintext highlighter-rouge">hugo serve</code> to test</p>

<p><a href="/content/images/2026/04/hugoingcp-66.png"><img src="/content/images/2026/04/hugoingcp-66.png" alt="/content/images/2026/04/hugoingcp-66.png" /></a></p>

<p>One thing I did different this time was give it a name with a date.  This will be handy for finding things over time</p>

<p><a href="/content/images/2026/04/hugoingcp-67.png"><img src="/content/images/2026/04/hugoingcp-67.png" alt="/content/images/2026/04/hugoingcp-67.png" /></a></p>

<p>We can then see that pushed to the test bucket</p>

<p><a href="/content/images/2026/04/hugoingcp-19.png"><img src="/content/images/2026/04/hugoingcp-19.png" alt="/content/images/2026/04/hugoingcp-19.png" /></a></p>

<p>However, when I checked the bucket, I realized I made a mistake.  It had copied the “public” folder in there instead of making the files at root</p>

<p><a href="/content/images/2026/04/hugoingcp-20.png"><img src="/content/images/2026/04/hugoingcp-20.png" alt="/content/images/2026/04/hugoingcp-20.png" /></a></p>

<p>I deleted it</p>

<p><a href="/content/images/2026/04/hugoingcp-21.png"><img src="/content/images/2026/04/hugoingcp-21.png" alt="/content/images/2026/04/hugoingcp-21.png" /></a></p>

<p>My goof was using “cp” instead of “rsync” in the branch upload</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
            <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
                <span class="s">echo "✅ On main branch, proceeding with GCS sync..."</span>
                <span class="s"># -r is recursive, -d deletes files in destination not in source (optional)</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme --recursive</span>
            <span class="s">else</span>
                <span class="s">echo "⚠️ Not on main branch, uploading to testing path."</span>
                <span class="s">gcloud storage cp ./public gs://dbeelogsme-test --recursive</span>
            <span class="s">fi</span>
</code></pre></div></div>

<p>I fixed it</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>            <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
                <span class="s">echo "✅ On main branch, proceeding with GCS sync..."</span>
                <span class="s"># -r is recursive, -d deletes files in destination not in source (optional)</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme --recursive</span>
            <span class="s">else</span>
                <span class="s">echo "⚠️ Not on main branch, uploading to testing path."</span>
                <span class="s">gcloud storage rsync ./public gs://dbeelogsme-test --recursive</span>
            <span class="s">fi</span>
</code></pre></div></div>

<h2 id="test-site">Test site</h2>

<p>Let’s run through that flow again for the “test” site</p>

<p>I’ll make a new static IP</p>

<p><a href="/content/images/2026/04/hugoingcp-22.png"><img src="/content/images/2026/04/hugoingcp-22.png" alt="/content/images/2026/04/hugoingcp-22.png" /></a></p>

<p>Then take the new static IPv4 (<code class="language-plaintext highlighter-rouge">34.54.122.70</code>)</p>

<p><a href="/content/images/2026/04/hugoingcp-23.png"><img src="/content/images/2026/04/hugoingcp-23.png" alt="/content/images/2026/04/hugoingcp-23.png" /></a></p>

<p>And use it in a new A record for <code class="language-plaintext highlighter-rouge">test.dbeelogs.me</code></p>

<p><a href="/content/images/2026/04/hugoingcp-24.png"><img src="/content/images/2026/04/hugoingcp-24.png" alt="/content/images/2026/04/hugoingcp-24.png" /></a></p>

<p>I can now create a new global ALB</p>

<p><a href="/content/images/2026/04/hugoingcp-25.png"><img src="/content/images/2026/04/hugoingcp-25.png" alt="/content/images/2026/04/hugoingcp-25.png" /></a></p>

<p>And create the certificate during the front-end configuration as before</p>

<p><a href="/content/images/2026/04/hugoingcp-29.png"><img src="/content/images/2026/04/hugoingcp-29.png" alt="/content/images/2026/04/hugoingcp-29.png" /></a></p>

<p>I’ll pick the test bucket (which I had set up some time ago)</p>

<p><a href="/content/images/2026/04/hugoingcp-68.png"><img src="/content/images/2026/04/hugoingcp-68.png" alt="/content/images/2026/04/hugoingcp-68.png" /></a></p>

<p>Then leave default routing rules and click create</p>

<p><a href="/content/images/2026/04/hugoingcp-69.png"><img src="/content/images/2026/04/hugoingcp-69.png" alt="/content/images/2026/04/hugoingcp-69.png" /></a></p>

<p>With the ALB created, I checked the “test” site, but did not see my new post</p>

<p><a href="/content/images/2026/04/hugoingcp-70.png"><img src="/content/images/2026/04/hugoingcp-70.png" alt="/content/images/2026/04/hugoingcp-70.png" /></a></p>

<p>Perhaps the bucket has the wrong contents.  I checked a direct storage URL:</p>

<p><a href="/content/images/2026/04/hugoingcp-71.png"><img src="/content/images/2026/04/hugoingcp-71.png" alt="/content/images/2026/04/hugoingcp-71.png" /></a></p>

<p>I went back to edit the backend configuration of the load balancer and instead of picking an existing bucket, I clicked “Create a backend bucket”</p>

<p><a href="/content/images/2026/04/hugoingcp-72.png"><img src="/content/images/2026/04/hugoingcp-72.png" alt="/content/images/2026/04/hugoingcp-72.png" /></a></p>

<p>An important step here is to <strong>uncheck</strong> the “Enable Cloud CDN” checkbox.  For a test site (which is just a preview of in-flux articles), we would not want any of them cached (as we are actively changing them)</p>

<p><a href="/content/images/2026/04/hugoingcp-73.png"><img src="/content/images/2026/04/hugoingcp-73.png" alt="/content/images/2026/04/hugoingcp-73.png" /></a></p>

<p>Lastly, uncheck the errant bucket (dbeelogsme), check the new endpoint bucket (dbeelogsmetest) and click update</p>

<p><a href="/content/images/2026/04/hugoingcp-74.png"><img src="/content/images/2026/04/hugoingcp-74.png" alt="/content/images/2026/04/hugoingcp-74.png" /></a></p>

<p>Oddly, I hit a quota issue when I did this</p>

<p><a href="/content/images/2026/04/hugoingcp-75.png"><img src="/content/images/2026/04/hugoingcp-75.png" alt="/content/images/2026/04/hugoingcp-75.png" /></a></p>

<p>This is an easy fix in the <a href="https://console.cloud.google.com/iam-admin/quotas">limits and quotas page</a></p>

<p><a href="/content/images/2026/04/hugoingcp-76.png"><img src="/content/images/2026/04/hugoingcp-76.png" alt="/content/images/2026/04/hugoingcp-76.png" /></a></p>

<p>setting a more reasonable limit</p>

<p><a href="/content/images/2026/04/hugoingcp-77.png"><img src="/content/images/2026/04/hugoingcp-77.png" alt="/content/images/2026/04/hugoingcp-77.png" /></a></p>

<p>Once I swapped backend buckets, I saw the site load as I hoped.  Moreover, it was a smidge slower in loading the larger images which was a good clue it was not using a CDN (the desired behaviour)</p>

<p><a href="/content/images/2026/04/hugoingcp-78.png"><img src="/content/images/2026/04/hugoingcp-78.png" alt="/content/images/2026/04/hugoingcp-78.png" /></a></p>

<h2 id="costs">Costs</h2>

<p>I saw some spikes but waited a day or so to see their origins.</p>

<p><a href="/content/images/2026/04/hugoingcp-79.png"><img src="/content/images/2026/04/hugoingcp-79.png" alt="/content/images/2026/04/hugoingcp-79.png" /></a></p>

<p>It would seem I was spending roughly 2.5c/hour on ALBs.  That means in a month I likely would be spending $20 for the main and test websites</p>

<p><a href="/content/images/2026/04/hugoingcp-80.png"><img src="/content/images/2026/04/hugoingcp-80.png" alt="/content/images/2026/04/hugoingcp-80.png" /></a></p>

<p>I ran that by the Cost calculator for GCP</p>

<p><a href="/content/images/2026/04/hugoingcp-81.png"><img src="/content/images/2026/04/hugoingcp-81.png" alt="/content/images/2026/04/hugoingcp-81.png" /></a></p>

<p>Indeed it would be just under US$20/mo for the ALBs.</p>

<p>However, what I found interesting is that I can do up to 5 of them for that price:</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/hugoingcp-82.mp4" type="video/mp4" />
</video>

<p>I let it sit for a few days and came back and saw it might even be less than I thought.  i have 3 Load Balancers presently (1 test, 2 for production with one being a redirect) and it looks to me like it might be closer to $15 for the month than $20</p>

<p><a href="/content/images/2026/04/hugogcpcost-01.png"><img src="/content/images/2026/04/hugogcpcost-01.png" alt="/content/images/2026/04/hugogcpcost-01.png" /></a></p>

<h2 id="cleanup">Cleanup</h2>

<p>However, I have another approach to try so let’s clean this one up</p>

<p>I’ll go to my <a href="https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers">Load Balancers page</a> and select each one to delete.</p>

<p>I will delete the “backend buckets” because they are just a pointer to storage buckets - it doesn’t actually delete the underlying bucket, just the ‘pointer’ “backend bucket”.  I do think the wording on this is confusing.  I will also delete the certs (they aren’t special, I can always re-create them).</p>

<p><a href="/content/images/2026/04/hugogcpcost-02.png"><img src="/content/images/2026/04/hugogcpcost-02.png" alt="/content/images/2026/04/hugogcpcost-02.png" /></a></p>

<p>I’ll delete any stray <a href="https://console.cloud.google.com/net-services/loadbalancing/list/backends">“backend buckets”</a></p>

<p><a href="/content/images/2026/04/hugogcpcost-03.png"><img src="/content/images/2026/04/hugogcpcost-03.png" alt="/content/images/2026/04/hugogcpcost-03.png" /></a></p>

<p>I’m <em>not</em> going to delete my actual buckets as I plan to do something new in my next post.  But if you were done with blogs, you could delete your storage buckets as well</p>

<p><a href="/content/images/2026/04/hugogcpcost-04.png"><img src="/content/images/2026/04/hugogcpcost-04.png" alt="/content/images/2026/04/hugogcpcost-04.png" /></a></p>

<p>Lastly, a step I often forget but won’t this time, is to cleanup DNS by deleting record sets</p>

<p><a href="/content/images/2026/04/hugogcpcost-05.png"><img src="/content/images/2026/04/hugogcpcost-05.png" alt="/content/images/2026/04/hugogcpcost-05.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>Coming from Azure, I believe this will ultimately be much much cheaper for hosting.  In the Azure model, we were <strong>required</strong> to have an Azure Front Door if we wanted proper certs and HTTPS but with Google, using the Cloud CDN is entirely optional.</p>

<p>I spent entirely too much time debugging the flow.  The fact is, GCP does not make the order of operations clear.  Never, when I was trying to get certs working, did it suggest to create a missing A Record, or setup the static IPs first.  At one point it made an unbound cert for <code class="language-plaintext highlighter-rouge">www.dbeelogs.me</code> that I couldn’t figure out how to use (as it wasn’t a ‘classic’ cert).</p>

<p>I did, in the end, go to the thinking mode of Gemini to help figure out the flow.  Once it suggested a specific order of operations (static IP, A record, ALB, then cert) I found my issues entirely went away.</p>

<p>Sometimes in the blog I will detail my bad flows but I wiped all those out since it would just waste yalls time.</p>

<p>Let’s talk about cost.</p>

<p>In Azure, <em>each</em> Front Door (standard) is US$35/mo + traffic.  So every ‘web site’ would basically cost $35 and then some.  For little blogs that would just be a non-starter (as getting something like a <a href="https://wordpress.com/">basic Wordpress account</a> is $2.75 or using <a href="https://docs.github.com/en/pages">Github pages</a> is basically free).</p>

<p>In GCP, having a persistent site is not free.  It is essentially US$20 (or less) for the Global Application Balancer (which can use the backend buckets).  However, the CDN aspect is very cheap (if you add it).  Additionally, as we saw, it’s <strong>not</strong> <em>$20/site</em> rather we can get up to 5 endpoints for US$20 before the price starts to go up.</p>

<p>I did not do AWS (yet), but mostly because I know AWS.  I use it today for this blog.  And the reason is because it is just stoopid cheap for hosting a static blog.</p>

<p>I’m only now getting upwards of $15/mo in AWS due to the large about of traffic (shouldn’t be too braggy - likely most of it are bots).  But for years i spent $3-5:</p>

<p><a href="/content/images/2026/04/hugoingcp-82.png"><img src="/content/images/2026/04/hugoingcp-82.png" alt="/content/images/2026/04/hugoingcp-82.png" /></a></p>

<p>And unlike Azure, the invalidation step with AWS is fast.</p>

<p>However, to lower costs over time, I trimming the <code class="language-plaintext highlighter-rouge">rsync</code> command to just handle the last couple months when i upload, which is some complicate logic</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">create sync command</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">#!/bin/bash</span>

            <span class="s"># Get the current month (numerical representation)</span>
            <span class="s">current_month=$(date +%m)</span>
            <span class="s">current_year=$(date +%Y)</span>

            <span class="s">last_month=$(( $current_month - 1 ))</span>
            <span class="s">last_year=$(( $current_year - 1 ))</span>

            <span class="s">if (( $current_month == 1 )); then</span>
              <span class="s">current_year=$last_year</span>
              <span class="s">current_month=12</span>
              <span class="s">last_month=$(( $current_month - 1 ))</span>
            <span class="s">fi</span>

            <span class="s">printf "aws s3 sync ./_site s3://freshbrewed.science --size-only" &gt; /tmp/synccmd.sh</span>

            <span class="s">for (( year=2019; year&lt;$current_year; year++ )); do</span>
                <span class="s">printf " --exclude 'content/images/%04d/*'" "$year" &gt;&gt; /tmp/synccmd.sh</span>
            <span class="s">done</span>

            <span class="s"># Loop through the months and print them up to the current month</span>
            <span class="s">for (( month_num=1; month_num&lt;last_month; month_num++ )); do </span>
                <span class="s">printf " --exclude 'content/images/%04d/%02d/*'" "$current_year" "$month_num" &gt;&gt; /tmp/synccmd.sh</span>
            <span class="s">done</span>
            <span class="s">printf " --acl public-read\n" &gt;&gt; /tmp/synccmd.sh</span>
            <span class="s">chmod 755 /tmp/synccmd.sh</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">copy files to final s3 fb</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">/tmp/synccmd.sh</span>
        <span class="na">env</span><span class="pi">:</span> <span class="c1"># Or as an environment variable</span>
          <span class="na">AWS_ACCESS_KEY_ID</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">AWS_SECRET_ACCESS_KEY</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">AWS_DEFAULT_REGION</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<p>I also had to not just invalidate the main <code class="language-plaintext highlighter-rouge">index.html</code> but very sub-page one (or the ordered pages would lose posts)</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">cloudfront invalidations</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">aws cloudfront create-invalidation --distribution-id E3U2HCN2ZRTBZN --paths "/index.html"</span>
            <span class="s"># Invalidate all index.html files, main and pages (.e.g page2/index.html)</span>
            <span class="s">cd _site</span>
            <span class="s">mapfile -t paths &lt; &lt;(find . -type f -name index.html -printf '/%P\n')</span>
            <span class="s">aws cloudfront create-invalidation --distribution-id E3U2HCN2ZRTBZN --paths "${paths[@]}"</span>
</code></pre></div></div>

<p>This makes me wonder, if our goal is “small blog hosting”, is perhaps serverless the way to go.  We pay for Load Balancers <em>per hour</em> and if we are honest, a huge amount of the time no one is looking.  Could a GCP Cloud Function, Azure Function, or AWS Lambda do a better job?  And would that be a <code class="language-plaintext highlighter-rouge">hugo</code> server? or a rendered static site served with an <code class="language-plaintext highlighter-rouge">nginx</code> container.   A lot of serverless costs has to do with memory asks and startup times (Nginx is really fast and has low memory demands).</p>

<p>But we’ll save all that for another day.</p>

<p>In summary, I think the Google solution, at least when compared to Azure, is a better value.  However, at this point, not enough to get me to migrate this site over (but we are getting close).</p>]]></content><author><name>Isaac Johnson</name></author><category term="blog" /><category term="staticwebsite" /><category term="GCP" /><category term="Hugo" /><category term="markdown" /><category term="forgejo" /><category term="gitea" /><category term="cicd" /><category term="cloudcdn" /><summary type="html"><![CDATA[In our last post we explored Hugo static sites with Azure, focusing on Azure Front Door and Storage Buckets. Today, we will look at doing a similar thing in GCP with Hugo and Cloud CDN and Application Load Balancers (ALBs).]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/gcpstaticsitebg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/gcpstaticsitebg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hugo and Azure Static Sites with Front Door</title><link href="https://freshbrewed.science/2026/04/16/hugo.html" rel="alternate" type="text/html" title="Hugo and Azure Static Sites with Front Door" /><published>2026-04-16T10:00:01+00:00</published><updated>2026-04-16T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/16/hugo</id><content type="html" xml:base="https://freshbrewed.science/2026/04/16/hugo.html"><![CDATA[<p>I’ve been using Jekyll for a long time - mostly since 2019 and in that time it’s grown a bit out of control.</p>

<p>I could just create an archive of past years and start over - that is a choice.  But I also want to explore other options.</p>

<p>The first that came up on good static site hosting was Hugo.</p>

<p>The other topic I want to dig into is static site hosting in Azure.  In AWS I used S3 with CloudFront.  It’s pretty inexpensive (when i was starting, just $3 or $4 a month.. Now it’s more like $16 with all the traffic we get).</p>

<p>How hard would it be to launch a new site with a proper DNS using Azure Storage and Azure FrontDoor CDN?</p>

<p>We’ll tackle all that and more (like CICD and Forgejo).  But first, let’s start with Hugo.</p>

<h1 id="installing-hugo-locally">Installing Hugo locally</h1>

<p>It’s easy to install the Hugo binary with snap</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo snap install hugo
[sudo: authenticate] Password:
hugo 0.160.0 from Hugo Authors installed
</code></pre></div></div>

<p>I’ll create a new project with the <code class="language-plaintext highlighter-rouge">new project</code> command</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces$ hugo new project initech
Congratulations! Your new Hugo project was created in /home/builder/Workspaces/initech.

Just a few more steps...

1. Change the current directory to /home/builder/Workspaces/initech.
2. Create or install a theme:
   - Create a new theme with the command "hugo new theme &lt;THEMENAME&gt;"
   - Or, install a theme from https://themes.gohugo.io/
3. Edit hugo.toml, setting the "theme" property to the theme name.
4. Create new content with the command "hugo new content &lt;SECTIONNAME&gt;/&lt;FILENAME&gt;.&lt;FORMAT&gt;".
5. Start the embedded web server with the command "hugo server --buildDrafts".

See documentation at https://gohugo.io/.
(base) builder@LuiGi:~/Workspaces$ cd initech/
(base) builder@LuiGi:~/Workspaces/initech$ git init
Initialized empty Git repository in /home/builder/Workspaces/initech/.git/
(base) builder@LuiGi:~/Workspaces/initech$
</code></pre></div></div>

<p>I didn’t want to use the theme from the tutorial so I picked a different one</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/zerostaticthemes/hugo-winston-theme.git themes/hugo-winston-theme
</code></pre></div></div>

<p>Since I did a git init, i figured it was safe to copy over their example content</p>

<p><a href="/content/images/2026/04/hugo-01.png"><img src="/content/images/2026/04/hugo-01.png" alt="/content/images/2026/04/hugo-01.png" /></a></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cp -a themes/hugo-winston-theme/exampleSite/. .
</code></pre></div></div>

<p>I can run <code class="language-plaintext highlighter-rouge">hugo server</code> to just test the content</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/initech$ hugo server
Watching for changes in /home/builder/Workspaces/initech/archetypes, /home/builder/Workspaces/initech/assets, /home/builder/Workspaces/initech/content/{pages,posts}, /home/builder/Workspaces/initech/data, /home/builder/Workspaces/initech/i18n, /home/builder/Workspaces/initech/layouts, /home/builder/Workspaces/initech/static/images
Watching for config changes in /home/builder/Workspaces/initech/hugo.toml
Start building sites …
hugo v0.160.0-652fc5acddf94e0501f778e196a8b630566b39ad+extended linux/amd64 BuildDate=2026-04-04T13:32:34Z VendorInfo=snap:0.160.0

WARN  found no layout file for "html" for kind "home": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
WARN  found no layout file for "html" for kind "taxonomy": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
WARN  found no layout file for "html" for kind "section": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
WARN  found no layout file for "html" for kind "page": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
WARN  found no layout file for "html" for kind "term": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
WARN  Raw HTML omitted while rendering "/home/builder/Workspaces/initech/content/posts/destruction-of-words.md"; see https://gohugo.io/getting-started/configuration-markup/#rendererunsafe
You can suppress this warning by adding the following to your project configuration:
ignoreLogs = ['warning-goldmark-raw-html']

                  │ EN
──────────────────┼────
 Pages            │  9
 Paginator pages  │  0
 Non-page files   │  0
 Static files     │  9
 Processed images │  0
 Aliases          │  0
 Cleaned          │  0

Built in 28 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
</code></pre></div></div>

<p>I had some issues with themes. I pivoted to <a href="https://github.com/CaiJimmy/hugo-theme-stack?tab=readme-ov-file">this one</a></p>

<p>In the end I used Gemini CLI to sort things out</p>

<p><a href="/content/images/2026/04/hugo-02.png"><img src="/content/images/2026/04/hugo-02.png" alt="/content/images/2026/04/hugo-02.png" /></a></p>

<p>which looked right when launched with <code class="language-plaintext highlighter-rouge">hugo server</code></p>

<p><a href="/content/images/2026/04/hugo-03.png"><img src="/content/images/2026/04/hugo-03.png" alt="/content/images/2026/04/hugo-03.png" /></a></p>

<p>The posts are organized in the <code class="language-plaintext highlighter-rouge">post</code> folder</p>

<p><a href="/content/images/2026/04/hugo-04.png"><img src="/content/images/2026/04/hugo-04.png" alt="/content/images/2026/04/hugo-04.png" /></a></p>

<p>I worked it a bit, changing settings in config/_default/hugo.toml and menu.toml.</p>

<p><a href="/content/images/2026/04/hugo-05.png"><img src="/content/images/2026/04/hugo-05.png" alt="/content/images/2026/04/hugo-05.png" /></a></p>

<h2 id="hosting">Hosting</h2>

<p>Let’s get a DNS name.  I think tpk.lat would work</p>

<p><a href="/content/images/2026/04/hugo-06.png"><img src="/content/images/2026/04/hugo-06.png" alt="/content/images/2026/04/hugo-06.png" /></a></p>

<p>I’ll next create a storage account</p>

<p><a href="/content/images/2026/04/hugo-07.png"><img src="/content/images/2026/04/hugo-07.png" alt="/content/images/2026/04/hugo-07.png" /></a></p>

<p>I’ll then make a new Azure DNS entry</p>

<p><a href="/content/images/2026/04/hugo-08.png"><img src="/content/images/2026/04/hugo-08.png" alt="/content/images/2026/04/hugo-08.png" /></a></p>

<p>To make it live, I need to take the nameservers from Azure</p>

<p><a href="/content/images/2026/04/hugo-09.png"><img src="/content/images/2026/04/hugo-09.png" alt="/content/images/2026/04/hugo-09.png" /></a></p>

<p>And then apply them in Gandi (or whatever Registrar you use)</p>

<p><a href="/content/images/2026/04/hugo-10.png"><img src="/content/images/2026/04/hugo-10.png" alt="/content/images/2026/04/hugo-10.png" /></a></p>

<p>I need to get a Front Door service started before I can really use the custom domain.</p>

<p>I enabled a new Front Door policy on the Storage Account and did enable caching, compression and a standard WAF</p>

<p><a href="/content/images/2026/04/azurefd-01.png"><img src="/content/images/2026/04/azurefd-01.png" alt="/content/images/2026/04/azurefd-01.png" /></a></p>

<p>It took a while, but eventually the FD resources were created</p>

<p><a href="/content/images/2026/04/azurefd-02.png"><img src="/content/images/2026/04/azurefd-02.png" alt="/content/images/2026/04/azurefd-02.png" /></a></p>

<p>I can now go to the Domains section in Front Door to add an Azure domain.  Since this is new, I have not validated it yet so I cannot say “pre-validated”.</p>

<p><a href="/content/images/2026/04/azurefd-03.png"><img src="/content/images/2026/04/azurefd-03.png" alt="/content/images/2026/04/azurefd-03.png" /></a></p>

<p>Clicking “Pending” will let me add the validation record they want</p>

<p><a href="/content/images/2026/04/azurefd-04.png"><img src="/content/images/2026/04/azurefd-04.png" alt="/content/images/2026/04/azurefd-04.png" /></a></p>

<p>It said it saved a TXT record, but I see no evidence of that:</p>

<p><a href="/content/images/2026/04/azurefd-05.png"><img src="/content/images/2026/04/azurefd-05.png" alt="/content/images/2026/04/azurefd-05.png" /></a></p>

<p>That’s when I realized I goofed up and used “.life” (the old domain) and not “.lat”.</p>

<p>I fixed it by doing the flow again, though this time i did an Apex domain instead of a CNAME to www</p>

<p><a href="/content/images/2026/04/azurefd-06.png"><img src="/content/images/2026/04/azurefd-06.png" alt="/content/images/2026/04/azurefd-06.png" /></a></p>

<p>Let me try validating it again, this time with the right domain</p>

<p><a href="/content/images/2026/04/azurefd-07.png"><img src="/content/images/2026/04/azurefd-07.png" alt="/content/images/2026/04/azurefd-07.png" /></a></p>

<p>Since I removed the old errant record an hour ago, when I refreshed I knew it added the TXT record:</p>

<p><a href="/content/images/2026/04/azurefd-08.png"><img src="/content/images/2026/04/azurefd-08.png" alt="/content/images/2026/04/azurefd-08.png" /></a></p>

<p>After a couple minutes, it changed to Validated:</p>

<p><a href="/content/images/2026/04/azurefd-09.png"><img src="/content/images/2026/04/azurefd-09.png" alt="/content/images/2026/04/azurefd-09.png" /></a></p>

<p>Now I can associate with an endpoint (the SA Front Door we just created)</p>

<p><a href="/content/images/2026/04/azurefd-10.png"><img src="/content/images/2026/04/azurefd-10.png" alt="/content/images/2026/04/azurefd-10.png" /></a></p>

<p>At the end of that line, it prompts you to “Create an alias record” (under “DNS State” column)</p>

<p><a href="/content/images/2026/04/azurefd-11.png"><img src="/content/images/2026/04/azurefd-11.png" alt="/content/images/2026/04/azurefd-11.png" /></a></p>

<p>I finally see everything “green”</p>

<p><a href="/content/images/2026/04/azurefd-12.png"><img src="/content/images/2026/04/azurefd-12.png" alt="/content/images/2026/04/azurefd-12.png" /></a></p>

<p>I now see a nice Azure style 404 when using HTTP, but an invalid cert for HTTPS</p>

<p><a href="/content/images/2026/04/azurefd-13.png"><img src="/content/images/2026/04/azurefd-13.png" alt="/content/images/2026/04/azurefd-13.png" /></a></p>

<p>Seems my custom domain is, at present, still serving up the ‘azureedge’ cert</p>

<p><a href="/content/images/2026/04/azurefd-14.png"><img src="/content/images/2026/04/azurefd-14.png" alt="/content/images/2026/04/azurefd-14.png" /></a></p>

<p>I’m going to wait a few hours to see if this is just a timing though before I force a re-update.  Indeed, it was just timing:</p>

<p><a href="/content/images/2026/04/azurefd-15.png"><img src="/content/images/2026/04/azurefd-15.png" alt="/content/images/2026/04/azurefd-15.png" /></a></p>

<p>I then created a git repo for the blog in Forgejo</p>

<p><a href="/content/images/2026/04/azurefd-16.png"><img src="/content/images/2026/04/azurefd-16.png" alt="/content/images/2026/04/azurefd-16.png" /></a></p>

<p>Then pushed up the Hugo blog as it was - however, I did make sure to <strong>not</strong> add “./public” which is the generated folder</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/initech$ git remote add origin https://forgejo.freshbrewed.science/builderadmin/hugo-initech.git
(base) builder@LuiGi:~/Workspaces/initech$ git push -u origin --all
Enumerating objects: 96, done.
Counting objects: 100% (96/96), done.
Delta compression using up to 16 threads
Compressing objects: 100% (85/85), done.
Writing objects: 100% (96/96), 16.92 MiB | 9.33 MiB/s, done.
Total 96 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
remote: . Processing 1 references
remote: Processed 1 references in total
To https://forgejo.freshbrewed.science/builderadmin/hugo-initech.git
 * [new branch]      main -&gt; main
branch 'main' set up to track 'origin/main'.
</code></pre></div></div>

<p>Trying on a different machine reminded me of two things:</p>

<ol>
  <li>We have submodules, so it’s not just clone, but also running a submodule update (<code class="language-plaintext highlighter-rouge">git submodule update --init --recursive</code>)</li>
  <li>Hugo versions are particular.  In my case, I needed to run <code class="language-plaintext highlighter-rouge">brew install hugo</code> to upgrade from the older version to the latest</li>
</ol>

<p>But then it worked just fine (now in WSL and on Windows)</p>

<p><a href="/content/images/2026/04/azurefd-17.png"><img src="/content/images/2026/04/azurefd-17.png" alt="/content/images/2026/04/azurefd-17.png" /></a></p>

<p>I just run <code class="language-plaintext highlighter-rouge">hugo</code> to ensure the ‘public’ folder is up to date</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ hugo
Start building sites …
hugo v0.160.0+extended+withdeploy linux/amd64 BuildDate=2026-04-04T13:32:34Z VendorInfo=Homebrew

WARN  deprecated: .Site.Data was deprecated in Hugo v0.156.0 and will be removed in a future release. Use hugo.Data instead.
WARN  Taxonomy categories not found
WARN  Taxonomy tags not found

              │ EN │ ZH │ ZH - HANT … │ JA
──────────────┼────┼────┼─────────────┼────
 Pages        │ 27 │ 17 │          17 │ 17
 Paginator    │  0 │  0 │           0 │  0
 pages        │    │    │             │
 Non-page     │  3 │  0 │           0 │  0
 files        │    │    │             │
 Static files │  0 │  0 │           0 │  0
 Processed    │ 13 │  0 │           0 │  0
 images       │    │    │             │
 Aliases      │ 10 │  5 │           5 │  5
 Cleaned      │  0 │  0 │           0 │  0

Total in 206 ms
</code></pre></div></div>

<p>I’m not sure if the contents are at the root of the container OR in a <code class="language-plaintext highlighter-rouge">$web</code> folder.  So let’s try a quick index file:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ echo "&lt;HTML&gt;&lt;BODY&gt;&lt;H1&gt;TESTING.&lt;/H1&gt;&lt;/BODY&gt;&lt;/HTML&gt;" | tee /mnt/c/Users/isaac/Downloads/index.html
&lt;HTML&gt;&lt;BODY&gt;&lt;H1&gt;TESTING.&lt;/H1&gt;&lt;/BODY&gt;&lt;/HTML&gt;
</code></pre></div></div>

<p>I only saw a <code class="language-plaintext highlighter-rouge">$logs</code> container presently, So I made a <code class="language-plaintext highlighter-rouge">$web</code> which is needed for Static Site hosting</p>

<p><a href="/content/images/2026/04/azurefd-18.png"><img src="/content/images/2026/04/azurefd-18.png" alt="/content/images/2026/04/azurefd-18.png" /></a></p>

<p>I was serving traffic, but getting an “OutOfRange” error</p>

<p><a href="/content/images/2026/04/azurefd-19.png"><img src="/content/images/2026/04/azurefd-19.png" alt="/content/images/2026/04/azurefd-19.png" /></a></p>

<p>Copilot failed to help (I tried that first in the Azure portal). Besides a paltry 500 character limit, it wouldn’t let me pick a front door:</p>

<p><a href="/content/images/2026/04/azurefd-20.png"><img src="/content/images/2026/04/azurefd-20.png" alt="/content/images/2026/04/azurefd-20.png" /></a></p>

<p>However, Gemini clued me into the problem - I neglected to turn on static website hosting (I thought just adding the Azure FD would do that automatically)</p>

<p><a href="/content/images/2026/04/azurefd-21.png"><img src="/content/images/2026/04/azurefd-21.png" alt="/content/images/2026/04/azurefd-21.png" /></a></p>

<p>I turned it on</p>

<p><a href="/content/images/2026/04/azurefd-22.png"><img src="/content/images/2026/04/azurefd-22.png" alt="/content/images/2026/04/azurefd-22.png" /></a></p>

<p>The storage account website looks right now</p>

<p><a href="/content/images/2026/04/azurefd-23.png"><img src="/content/images/2026/04/azurefd-23.png" alt="/content/images/2026/04/azurefd-23.png" /></a></p>

<p>I then had to do this origin swap as it had an old “blob” endpoint stuck there.</p>

<p>I needed to find the origin that was there with the routes:</p>

<p><a href="/content/images/2026/04/azurefd-25.png"><img src="/content/images/2026/04/azurefd-25.png" alt="/content/images/2026/04/azurefd-25.png" /></a></p>

<p>Add a new route to the “web.core” endpoint</p>

<p><a href="/content/images/2026/04/azurefd-26.png"><img src="/content/images/2026/04/azurefd-26.png" alt="/content/images/2026/04/azurefd-26.png" /></a></p>

<p>Then associate and unassociate the old then then delete so it ended up looking like</p>

<p><a href="/content/images/2026/04/azurefd-27.png"><img src="/content/images/2026/04/azurefd-27.png" alt="/content/images/2026/04/azurefd-27.png" /></a></p>

<p>But when done, and after about 15m, it started to serve properly</p>

<p><a href="/content/images/2026/04/azurefd-24.png"><img src="/content/images/2026/04/azurefd-24.png" alt="/content/images/2026/04/azurefd-24.png" /></a></p>

<p>next, I uploaded the site - this was actually really easy using the <code class="language-plaintext highlighter-rouge">az</code> CLI</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ az storage blob upload-batch --account-name tpklat -d '$web' -s ./public
Command group 'az storage' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus

There are no credentials provided in your command and environment, we will query for account key for your storage account.
It is recommended to provide --connection-string, --account-key or --sas-token in your command as credentials.

You also can add `--auth-mode login` in your command to use Azure Active Directory (Azure AD) for authorization if your login account is assigned required RBAC roles.
For more information about RBAC roles in storage, visit https://learn.microsoft.com/azure/storage/common/storage-auth-aad-rbac-cli.

In addition, setting the corresponding environment variables can avoid inputting credentials in your command. Please use --help to get more information about environment variable usage.
4/125: "404.html"[####################################################]  100.0000%The specified blob already exists.
RequestId:614e826c-601e-0098-78bc-c62ded000000
Time:2026-04-07T18:29:01.2582855Z
ErrorCode:BlobAlreadyExists
If you want to overwrite the existing one, please add --overwrite in your command.
Finished[#############################################################]  100.0000%.js"[]  100.0000%100.0000%
1 of 125 files not uploaded due to "Failed Precondition"
[
  {
    "Blob": "https://tpklat.blob.core.windows.net/%24web/GitHub-Mark_866618693131658625_hu_9d3935265776b641.png",
    "Last Modified": "2026-04-07T18:29:00+00:00",
    "Type": "image/png",
    "eTag": "\"0x8DE94D38A21B14E\""
  },

  ... snip ...
</code></pre></div></div>

<p>I only had to force 2 files:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ az storage blob upload --account-name tpklat --container-name '$web' --name index.html --file ./public/index.html --overwrite
Command group 'az storage' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus

There are no credentials provided in your command and environment, we will query for account key for your storage account.
It is recommended to provide --connection-string, --account-key or --sas-token in your command as credentials.

You also can add `--auth-mode login` in your command to use Azure Active Directory (Azure AD) for authorization if your login account is assigned required RBAC roles.
For more information about RBAC roles in storage, visit https://learn.microsoft.com/azure/storage/common/storage-auth-aad-rbac-cli.

In addition, setting the corresponding environment variables can avoid inputting credentials in your command. Please use --help to get more information about environment variable usage.
Finished[#############################################################]  100.0000%
{
  "client_request_id": "c7bf0756-32af-11f1-ab3e-00155d325100",
  "content_md5": "iQzYYP/MN17DQS7MMPDpbw==",
  "date": "2026-04-07T18:29:58+00:00",
  "encryption_key_sha256": null,
  "encryption_scope": null,
  "etag": "\"0x8DE94D3AC5E45D9\"",
  "lastModified": "2026-04-07T18:29:58+00:00",
  "request_id": "0fe4678d-801e-0090-23bc-c637e2000000",
  "request_server_encrypted": true,
  "version": "2022-11-02",
  "version_id": null
}
builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ az storage blob upload --account-name tpklat --container-name '$web' --name 404.html --file ./public/404.html --overwrite
Command group 'az storage' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus

There are no credentials provided in your command and environment, we will query for account key for your storage account.
It is recommended to provide --connection-string, --account-key or --sas-token in your command as credentials.

You also can add `--auth-mode login` in your command to use Azure Active Directory (Azure AD) for authorization if your login account is assigned required RBAC roles.
For more information about RBAC roles in storage, visit https://learn.microsoft.com/azure/storage/common/storage-auth-aad-rbac-cli.

In addition, setting the corresponding environment variables can avoid inputting credentials in your command. Please use --help to get more information about environment variable usage.
Finished[#############################################################]  100.0000%
{
  "client_request_id": "cfb387b6-32af-11f1-8426-00155d325100",
  "content_md5": "toulLT+XQF2QsfgesRE7vw==",
  "date": "2026-04-07T18:30:10+00:00",
  "encryption_key_sha256": null,
  "encryption_scope": null,
  "etag": "\"0x8DE94D3B4579FDE\"",
  "lastModified": "2026-04-07T18:30:11+00:00",
  "request_id": "f105731f-d01e-00ff-4dbc-c63d11000000",
  "request_server_encrypted": true,
  "version": "2022-11-02",
  "version_id": null
}
</code></pre></div></div>

<p>I tried to purge the cache for just index.html (as index.html and 404.html were the only conflicts)</p>

<p><a href="/content/images/2026/04/azurefd-28.png"><img src="/content/images/2026/04/azurefd-28.png" alt="/content/images/2026/04/azurefd-28.png" /></a></p>

<p>But even after hours, it still showed the “TESTING” page.</p>

<p>I then tried a heavier purge</p>

<p><a href="/content/images/2026/04/azurefd-29.png"><img src="/content/images/2026/04/azurefd-29.png" alt="/content/images/2026/04/azurefd-29.png" /></a></p>

<p>which definitely worked</p>

<p><a href="/content/images/2026/04/azurefd-31.png"><img src="/content/images/2026/04/azurefd-31.png" alt="/content/images/2026/04/azurefd-31.png" /></a></p>

<h2 id="cicd">CICD</h2>

<p>We got a great start, but I want this blog to publish on merge to main.</p>

<p>First, I need to enable Repository Actions:</p>

<p><a href="/content/images/2026/04/azurefd-32.png"><img src="/content/images/2026/04/azurefd-32.png" alt="/content/images/2026/04/azurefd-32.png" /></a></p>

<p>I’ll start with a simple workflow just to make sure the system is working:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ mkdir -p .gitea/workflows
builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ vi .gitea/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ cat .gitea/workflows/cicd.yaml
name: Gitea Actions Test
run-name: $ is testing out Gitea Actions 🚀
on: [push]

jobs:
  Explore-Gitea-Actions:
    runs-on: my_custom_label
    container: node:22
    steps:
      - run: echo "🎉 The job was automatically triggered by a $ event."
      - run: echo "🐧 This job is now running on a $ server hosted by Gitea!"
      - run: echo "🔎 The name of your branch is $ and your repository is $."
      - name: Check out repository code
        uses: actions/checkout@v3
      - run: echo "💡 The $ repository has been cloned to the runner."
      - run: echo "🖥️ The workflow is now ready to test your code on the runner."
      - name: List files in the repository
        run: |
          ls $
      - run: echo "🍏 This job's status is $."
builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ git add .gitea/
builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ git commit -m "first runner"
[main 8585a78] first runner
 1 file changed, 19 insertions(+)
 create mode 100644 .gitea/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 789 bytes | 789.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To https://forgejo.freshbrewed.science/builderadmin/hugo-initech.git
   6a77f04..8585a78  main -&gt; main
</code></pre></div></div>

<p>which shows it at least runs</p>

<p><a href="/content/images/2026/04/azurefd-33.png"><img src="/content/images/2026/04/azurefd-33.png" alt="/content/images/2026/04/azurefd-33.png" /></a></p>

<p>Since I know I’m going to need to use Azure Storage in my flow eventually, I decided to setup the CICD workflow to use the <code class="language-plaintext highlighter-rouge">az</code> CLI and <code class="language-plaintext highlighter-rouge">hugo</code> cli right away</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>name: Gitea Actions Test
run-name: $ is testing out Gitea Actions 🚀
on: [push]

jobs:
  Explore-Gitea-Actions:
    runs-on: my_custom_label
    container: node:22
    steps:
      - run: |
          DEBIAN_FRONTEND=noninteractive apt update -y
          umask 0002
          DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo
          # Install MS Key
          
          # Use the official Microsoft script to handle repo mapping automatically
          curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
      - run: |
          echo "🔍 Checking Azure CLI version..."
          az --version
      - run: |
          DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip
      - run: |
          echo "🔍 Checking Hugo version..."
          hugo version
</code></pre></div></div>

<p><a href="/content/images/2026/04/azurefd-34.png"><img src="/content/images/2026/04/azurefd-34.png" alt="/content/images/2026/04/azurefd-34.png" /></a></p>

<p>Next, I decided to create a “testing” container I can use to see candidate releases</p>

<p><a href="/content/images/2026/04/azurefd-35.png"><img src="/content/images/2026/04/azurefd-35.png" alt="/content/images/2026/04/azurefd-35.png" /></a></p>

<p>In my Forgejo actions page, I will want to add some secrets to access this account</p>

<p><a href="/content/images/2026/04/azurefd-36.png"><img src="/content/images/2026/04/azurefd-36.png" alt="/content/images/2026/04/azurefd-36.png" /></a></p>

<p>Once I had the account and key (in “Security + networking/Access Keys”) saved as secrets</p>

<p><a href="/content/images/2026/04/azurefd-37.png"><img src="/content/images/2026/04/azurefd-37.png" alt="/content/images/2026/04/azurefd-37.png" /></a></p>

<p>My new Forgejo workflow looks like</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Gitea Actions Test</span>
<span class="na">run-name</span><span class="pi">:</span> <span class="s">$ is testing out Gitea Actions 🚀</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">Explore-Gitea-Actions</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">my_custom_label</span>
    <span class="na">container</span><span class="pi">:</span> <span class="s">node:22</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt update -y</span>
          <span class="s">umask 0002</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo</span>
          <span class="s"># Install MS Key</span>
          
          <span class="s"># Use the official Microsoft script to handle repo mapping automatically</span>
          <span class="s">curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Azure CLI version..."</span>
          <span class="s">az --version</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Hugo version..."</span>
          <span class="s">hugo version</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check out repository code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
          <span class="s">hugo</span>
          <span class="s">zip -r public.zip public</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">hugo</span>
          <span class="s">zip -r public.zip public</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">HUGO_ENV</span><span class="pi">:</span> <span class="s">production</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">az storage blob upload --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY --container-name testing --name public-$GITHUB_RUN_NUMBER.zip --file ./public.zip --overwrite</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">AZSTORAGE_ACCOUNT</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">AZSTORAGE_KEY</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">echo "🖥️ The workflow is now ready to test your code on the runner."</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">List files in the repository</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">ls $</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">echo "🍏 This job's status is $."</span>
</code></pre></div></div>

<p>It ran</p>

<p><a href="/content/images/2026/04/azurefd-38.png"><img src="/content/images/2026/04/azurefd-38.png" alt="/content/images/2026/04/azurefd-38.png" /></a></p>

<p>and I can see a candidate release with the build ID (see URL above) in the testing container</p>

<p><a href="/content/images/2026/04/azurefd-39.png"><img src="/content/images/2026/04/azurefd-39.png" alt="/content/images/2026/04/azurefd-39.png" /></a></p>

<p>didn’t look right so I tried without production “HUGO_ENV: production”.</p>

<p>The reason I only saw XMLs was that it wasn’t actually pulling in the theme submodule.</p>

<p>Once I updated the checkout action with submodules set to “recursive”</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      - name: Check out repository code
        uses: actions/checkout@v3
        with:
          submodules: recursive
</code></pre></div></div>

<p>Then it worked (to the point of telling my hugo was too old).  I pivoted to downloading the latest (as of this writing) from Github and then it worked</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Gitea Actions Test</span>
<span class="na">run-name</span><span class="pi">:</span> <span class="s">$ is testing out Gitea Actions 🚀</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">Explore-Gitea-Actions</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">my_custom_label</span>
    <span class="na">container</span><span class="pi">:</span> <span class="s">node:22</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt update -y</span>
          <span class="s">umask 0002</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo zip</span>
          <span class="s"># Install MS Key</span>
          
          <span class="s"># Use the official Microsoft script to handle repo mapping automatically</span>
          <span class="s">curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Azure CLI version..."</span>
          <span class="s">az --version</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check out repository code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">submodules</span><span class="pi">:</span> <span class="s">recursive</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip</span>
          <span class="s">wget https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_0.160.0_linux-amd64.tar.gz</span>
          <span class="s">tar -xzvf hugo_0.160.0_linux-amd64.tar.gz</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Hugo version..."</span>
          <span class="s">pwd</span>
          <span class="s">./hugo version</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">./hugo</span>
          <span class="s">zip -r public.zip public</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">az storage blob upload --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY --container-name testing --name public-$GITHUB_RUN_NUMBER.zip --file ./public.zip --overwrite</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">AZSTORAGE_ACCOUNT</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">AZSTORAGE_KEY</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">echo "🖥️ The workflow is now ready to test your code on the runner."</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">List files in the repository</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">ls $</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">echo "🍏 This job's status is $."</span>
</code></pre></div></div>

<p>The next part requires a SP as we will not only want to upload to the main ‘$web’ container, but expire the cache in AFD.</p>

<p>I’ll create a new low privileged SP in my Sub</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ az ad sp create-for-rbac \
  --name "Gitea-AFD-Purge-SP" \
  --role "Contributor" \
  --scopes "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/bloggingTestRG" \
  --sdk-auth
</code></pre></div></div>

<p>I set the secrets that came back in my pipeline and used it to purge the Front Door site, but only if on main</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>name: Gitea Actions Test
run-name: $ is testing out Gitea Actions 🚀
on: [push]

jobs:
  Explore-Gitea-Actions:
    runs-on: my_custom_label
    container: node:22
    steps:
      - run: |
          DEBIAN_FRONTEND=noninteractive apt update -y
          umask 0002
          DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo zip
          # Install MS Key
          
          # Use the official Microsoft script to handle repo mapping automatically
          curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
      - run: |
          echo "🔍 Checking Azure CLI version..."
          az --version
      - name: Check out repository code
        uses: actions/checkout@v3
        with:
          submodules: recursive
      - run: |
          # DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip
          wget https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_0.160.0_linux-amd64.tar.gz
          tar -xzvf hugo_0.160.0_linux-amd64.tar.gz
      - run: |
          echo "🔍 Checking Hugo version..."
          pwd
          ./hugo version
      - run: |
          export
          ls
          ls -ltra themes/hugo-theme-stack
      - run: |
          ./hugo
          zip -r public.zip public
      - name: Branch check and upload
        shell: bash
        run: |
          if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then
            echo "✅ On main branch, proceeding with Azure Blob upload..."
            az storage blob upload-batch --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY -d '$web' -s ./public
          else
            echo "⚠️ Not on main branch, uploading to testing container."
            az storage blob upload --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY --container-name testing --name public-$GITHUB_RUN_NUMBER.zip --file ./public.zip --overwrite
          fi  
        env:
          AZSTORAGE_ACCOUNT: $
          AZSTORAGE_KEY: $

      - name: Front Door cache purge
        shell: bash
        run: |
          if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then
            az login --service-principal -u $ -p $ --tenant $
            az afd endpoint purge \
                --subscription $ \
                --resource-group bloggingTestRG \
                --profile-name ttpklat \
                --endpoint-name tpklat-ame5egeghugpbed6.z01.azurefd.net \
                --domains tpk.lat \
                --content-paths '/*'
          else
            echo "⚠️ Not on main branch, skipping Azure Front Door purge."
          fi
</code></pre></div></div>

<p>We can now see the upload step working:</p>

<p><a href="/content/images/2026/04/azurefd-40.png"><img src="/content/images/2026/04/azurefd-40.png" alt="/content/images/2026/04/azurefd-40.png" /></a></p>

<p>as well as the purge</p>

<p><a href="/content/images/2026/04/azurefd-41.png"><img src="/content/images/2026/04/azurefd-41.png" alt="/content/images/2026/04/azurefd-41.png" /></a></p>

<h2 id="testing-the-flow">Testing the flow</h2>

<p>I wrote a new post and pushed up the branch</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ git push --set-upstream origin test-new-post
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 16 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (10/10), 2.37 MiB | 2.16 MiB/s, done.
Total 10 (delta 3), reused 0 (delta 0), pack-reused 0
remote: 
remote: Create a new pull request for 'test-new-post':
remote:   https://forgejo.freshbrewed.science/builderadmin/hugo-initech/compare/main...test-new-post
remote: 
remote: . Processing 1 references
remote: Processed 1 references in total
To https://forgejo.freshbrewed.science/builderadmin/hugo-initech.git
 * [new branch]      test-new-post -&gt; test-new-post
Branch 'test-new-post' set up to track remote branch 'test-new-post' from 'origin'.
</code></pre></div></div>

<p>I can see in my workflow it pushed a private copy up</p>

<p><a href="/content/images/2026/04/azurefd-42.png"><img src="/content/images/2026/04/azurefd-42.png" alt="/content/images/2026/04/azurefd-42.png" /></a></p>

<p>A really easy way to test is to download and extract the folder then just use a python one-liner to serve it up</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/hugo-initech$ cd /mnt/c/Users/isaac/Downloads/public23/
builder@DESKTOP-QADGF36:/mnt/c/Users/isaac/Downloads/public23$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
127.0.0.1 - - [08/Apr/2026 12:19:37] "GET / HTTP/1.1" 200 -
</code></pre></div></div>

<p><a href="/content/images/2026/04/azurefd-43.png"><img src="/content/images/2026/04/azurefd-43.png" alt="/content/images/2026/04/azurefd-43.png" /></a></p>

<p>This looks good, so I’ll merge it.  I like PR flows so I’ll use that</p>

<p><a href="/content/images/2026/04/azurefd-44.png"><img src="/content/images/2026/04/azurefd-44.png" alt="/content/images/2026/04/azurefd-44.png" /></a></p>

<p>It’s now merged and the branch is deleted</p>

<p><a href="/content/images/2026/04/azurefd-45.png"><img src="/content/images/2026/04/azurefd-45.png" alt="/content/images/2026/04/azurefd-45.png" /></a></p>

<p>The new merged commit to main properly uploaded and purged AFD</p>

<p><a href="/content/images/2026/04/azurefd-46.png"><img src="/content/images/2026/04/azurefd-46.png" alt="/content/images/2026/04/azurefd-46.png" /></a></p>

<p>However, even 15m later, the old site is still live.</p>

<p>I worked on this for a few hours.  I had two issues - I neglected to put the <code class="language-plaintext highlighter-rouge">--overwrite</code> on the az upload step for main (so it was not updating).</p>

<p>The other issue is that the ‘endpoint-name’ is not the hostname, but rather some sort version of a name (shown when you do “purge” in the Azure Portal)</p>

<p>e.g.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az afd endpoint purge \
    --subscription $SUBSCRIPTION \
    --resource-group bloggingTestRG \
    --profile-name ttpklat \
    --endpoint-name tpklat \
    --domains tpk.lat \
    --content-paths '/*'
</code></pre></div></div>

<p>The working version of the workflow is:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">$ cat .gitea/workflows/cicd.yaml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Gitea Actions Test</span>
<span class="na">run-name</span><span class="pi">:</span> <span class="s">$ is testing out Gitea Actions 🚀</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">Explore-Gitea-Actions</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">my_custom_label</span>
    <span class="na">container</span><span class="pi">:</span> <span class="s">node:22</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt update -y</span>
          <span class="s">umask 0002</span>
          <span class="s">DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates curl apt-transport-https lsb-release gnupg build-essential sudo zip</span>
          <span class="s"># Install MS Key</span>

          <span class="s"># Use the official Microsoft script to handle repo mapping automatically</span>
          <span class="s">curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Azure CLI version..."</span>
          <span class="s">az --version</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check out repository code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">submodules</span><span class="pi">:</span> <span class="s">recursive</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># DEBIAN_FRONTEND=noninteractive sudo apt install -y hugo zip</span>
          <span class="s">wget https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_0.160.0_linux-amd64.tar.gz</span>
          <span class="s">tar -xzvf hugo_0.160.0_linux-amd64.tar.gz</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "🔍 Checking Hugo version..."</span>
          <span class="s">pwd</span>
          <span class="s">./hugo version</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">export</span>
          <span class="s">ls</span>
          <span class="s">ls -ltra themes/hugo-theme-stack</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">./hugo</span>
          <span class="s">zip -r public.zip public</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Branch check and upload</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
            <span class="s">echo "✅ On main branch, proceeding with Azure Blob upload..."</span>
            <span class="s">az storage blob upload-batch --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY -d '$web' -s ./public --overwrite</span>
          <span class="s">else</span>
            <span class="s">echo "⚠️ Not on main branch, uploading to testing container."</span>
            <span class="s">az storage blob upload --account-name $AZSTORAGE_ACCOUNT --account-key $AZSTORAGE_KEY --container-name testing --name public-$GITHUB_RUN_NUMBER.zip --file ./public.zip --overwrite</span>
          <span class="s">fi</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">AZSTORAGE_ACCOUNT</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">AZSTORAGE_KEY</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Front Door cache purge</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [[ "$GITHUB_REF_NAME" == "main" &amp;&amp; "$GITHUB_REF_TYPE" == "branch" ]]; then</span>
            <span class="s">az login --service-principal -u $ -p $ --tenant $</span>
            <span class="s">az afd endpoint purge \</span>
                <span class="s">--subscription $ \</span>
                <span class="s">--resource-group bloggingTestRG \</span>
                <span class="s">--profile-name ttpklat \</span>
                <span class="s">--endpoint-name tpklat \</span>
                <span class="s">--domains tpk.lat \</span>
                <span class="s">--content-paths '/*'</span>
          <span class="s">else</span>
            <span class="s">echo "⚠️ Not on main branch, skipping Azure Front Door purge."</span>
          <span class="s">fi</span>
</code></pre></div></div>

<p>And finally I can see the site updated!</p>

<p><a href="/content/images/2026/04/azurefd-47.png"><img src="/content/images/2026/04/azurefd-47.png" alt="/content/images/2026/04/azurefd-47.png" /></a></p>

<h1 id="costs">Costs</h1>

<p>My largest concern is the Azure Front Door standard cost.  Just to <em>have</em> the CDN, let alone serve traffic looks like it will cost me US$35/month</p>

<p><a href="/content/images/2026/04/azurefd-48.png"><img src="/content/images/2026/04/azurefd-48.png" alt="/content/images/2026/04/azurefd-48.png" /></a></p>

<p>Compared to AWS’s CloudFront which starts at the very nice price of $0</p>

<p><a href="/content/images/2026/04/azurefd-49.png"><img src="/content/images/2026/04/azurefd-49.png" alt="/content/images/2026/04/azurefd-49.png" /></a></p>

<p>Azure <em>used</em> to cost $0.05/month (per <a href="https://www.reddit.com/r/AZURE/comments/1hbpdl8/static_website_azure_frontdoor_classic_standard/">this thread</a>).</p>

<p>However, even though it has a year left of support, one cannot make a “Classic” instance anymore</p>

<p><a href="/content/images/2026/04/azurefd-50.png"><img src="/content/images/2026/04/azurefd-50.png" alt="/content/images/2026/04/azurefd-50.png" /></a></p>

<p>In just two days my bill was up to $0.84 for this site</p>

<p><a href="/content/images/2026/04/azurefd-51.png"><img src="/content/images/2026/04/azurefd-51.png" alt="/content/images/2026/04/azurefd-51.png" /></a></p>

<p>The next day it was up to $1.61</p>

<p><a href="/content/images/2026/04/azurefd-52.png"><img src="/content/images/2026/04/azurefd-52.png" alt="/content/images/2026/04/azurefd-52.png" /></a></p>

<h2 id="using-blob-web-hosting">using blob web hosting</h2>

<p>Let’s start by creating a CNAME record that points to “tpklat.blob.core.windows.net”</p>

<p><a href="/content/images/2026/04/azurefd-53.png"><img src="/content/images/2026/04/azurefd-53.png" alt="/content/images/2026/04/azurefd-53.png" /></a></p>

<p>I can now use “Custom Domain” in the networking section of the storage account</p>

<p><a href="/content/images/2026/04/azurefd-54.png"><img src="/content/images/2026/04/azurefd-54.png" alt="/content/images/2026/04/azurefd-54.png" /></a></p>

<p>Arguably it <em>does</em> serve up the site, however it has to have the container name packed in there and the cert is signed for “blog.core.windows.net” so anyone visiting would get bad certs:</p>

<p><a href="/content/images/2026/04/azurefd-55.png"><img src="/content/images/2026/04/azurefd-55.png" alt="/content/images/2026/04/azurefd-55.png" /></a></p>

<p>By the next day after removving AFD, I see that little experiment cost me about $2.43</p>

<p><a href="/content/images/2026/04/azurefd-58.png"><img src="/content/images/2026/04/azurefd-58.png" alt="/content/images/2026/04/azurefd-58.png" /></a></p>

<p>We can see the details in Cost Analysis as well</p>

<p><a href="/content/images/2026/04/azurefd-60.png"><img src="/content/images/2026/04/azurefd-60.png" alt="/content/images/2026/04/azurefd-60.png" /></a></p>

<p>And triggered a page the next morning</p>

<p><a href="/content/images/2026/04/azurefd-59.png"><img src="/content/images/2026/04/azurefd-59.png" alt="/content/images/2026/04/azurefd-59.png" /></a></p>

<p>As an aside, I <strong>highly</strong> recommend using Cloud Budgets with a paging service like <a href="https://pagerduty.com">Pagerduty</a> to avoid ever getting blindsided by costs.  I have a writeup on setting those up across all cloud providers <a href="https://freshbrewed.science/2023/09/21/budgetsrevisit.html">here</a>.</p>

<h1 id="cleanup">Cleanup</h1>

<p>I’m not about to pay $35+ for a simple static blog and even if I fixed the routing with <code class="language-plaintext highlighter-rouge">$web</code> in the path, self-signed certs are a non starter.</p>

<p>While i do have a legacy AFD Classic I <em>could</em> re-purpose, it will stop managing SSL certs in less than a week of this writing so it’s pointless to use:</p>

<p><a href="/content/images/2026/04/azurefd-56.png"><img src="/content/images/2026/04/azurefd-56.png" alt="/content/images/2026/04/azurefd-56.png" /></a></p>

<p>I can opt to address the storage account in due time, but the Azure Front Door has to go - that’s what’s chewing up the spend.</p>

<p>We can just delete the AFD from the Front Doors blade</p>

<p><a href="/content/images/2026/04/azurefd-57.png"><img src="/content/images/2026/04/azurefd-57.png" alt="/content/images/2026/04/azurefd-57.png" /></a></p>

<p>While they don’t cost <em>much</em>, they do cost, so cleaning up old DNS zones is also advised to avoid incuring unneccessary costs</p>

<p><a href="/content/images/2026/04/azurefd-61.png"><img src="/content/images/2026/04/azurefd-61.png" alt="/content/images/2026/04/azurefd-61.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>We covered a lot today.  It started by just figuring out how to get Hugo up and running.  I would say as a tool it is easy (much easier than fighting Ruby dependencies with Jekyll), but the theme management left a bit to be desired.</p>

<p>With AI tools, however, it was quick work to sort out what was missing with my configuration.</p>

<p>Setting up static website hosting in Azure was quick work with Azure Storage + <a href="https://azure.microsoft.com/en-us/products/frontdoor">Front Door</a>.  I had some minor issues with naming conventions, but soon had traffic served on a new URL rather easily.</p>

<p>Moving to <a href="https://forgejo.org/">Forgejo</a> (which fundamentally is based on <a href="https://about.gitea.com/">Gitea</a>), it was easy to add Gitea Actions (CICD workflow) to take merged PRs and copy them out to Azure Storage.  I spent a lot more time than I thought figuring out how to get Azure Front Door to purge the cache.</p>

<p>I didn’t talk about this much above, but that AFD purge step is <strong>SLOW</strong> .  Each run would take about 15+ minutes (whether locally or in a pipeline step).</p>

<p>I could likely live with that (as it would only matter on publish), but the exorbitant price of AFD Standard is just not something I’m willing to accept.  Having to pay US$35 a month for the <em>base</em> price, not including data is just too damn high.  AWS doesn’t do that. Google doesn’t do that.</p>

<p>And even if we used static hosting in Azure Storage, having a ‘blob.core.windows’ URL is silly.  I can apply a custom domain with a CNAME, but they don’t update the cert, so it’s rather pointless.</p>

<p>Suprinsingly, when I went to verify it was scrubbed up, I found the ‘web.core’ address was still live.  So I could reach it at <a href="https://tpklat.z13.web.core.windows.net/">https://tpklat.z13.web.core.windows.net/</a></p>

<video muted="" controls="">
    <source src="/content/images/2026/04/azurefd-62.mp4" type="video/mp4" />
</video>

<p>My next step will be to look at a Google solution which I think might be far more cost effective.</p>]]></content><author><name>Isaac Johnson</name></author><category term="blog" /><category term="staticwebsite" /><category term="Azure" /><category term="Hugo" /><category term="markdown" /><category term="forgejo" /><category term="gitea" /><category term="cicd" /><category term="frontdoor" /><summary type="html"><![CDATA[I’ve been using Jekyll for a long time - mostly since 2019 and in that time it’s grown a bit out of control.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/azurefdhugo.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/azurefdhugo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Microsoft AI Image Gen: MAI</title><link href="https://freshbrewed.science/2026/04/13/aiimage.html" rel="alternate" type="text/html" title="Microsoft AI Image Gen: MAI" /><published>2026-04-13T10:00:01+00:00</published><updated>2026-04-13T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/13/aiimage</id><content type="html" xml:base="https://freshbrewed.science/2026/04/13/aiimage.html"><![CDATA[<p>Microsoft recently rolled out <a href="https://playground.microsoft.ai/chat">MAI</a>, a Microsoft AI playground that can do images.  Just images.  I learned about it from <a href="https://decrypt.co/361791/microsoft-mai-image-2-text-image-model-review">this Decrypt article</a>.  I wanted to try it and compare its performance and output to some of the other tools I use often; Midjourney, Gemini (Nanabanana 2) and Copilot itself.</p>

<p>I’ll look at a few different types of generation including horror, nice images, retro (likely copyrighted), diagrams and lettering.</p>

<p>Let’s start with good old Gordon Shumway…</p>

<h1 id="mai-2">MAI 2</h1>

<p>I tried a few ways to ask for ALF</p>

<p><a href="/content/images/2026/04/mai-01.png"><img src="/content/images/2026/04/mai-01.png" alt="/content/images/2026/04/mai-01.png" /></a></p>

<p>Even the most generic was blocked</p>

<p><a href="/content/images/2026/04/mai-02.png"><img src="/content/images/2026/04/mai-02.png" alt="/content/images/2026/04/mai-02.png" /></a></p>

<p>I could do an alien in a kitchen at least</p>

<p><a href="/content/images/2026/04/mai-03.png"><img src="/content/images/2026/04/mai-03.png" alt="/content/images/2026/04/mai-03.png" /></a></p>

<p>As soon as I asked for refinement it lost the context of the kitchen, however.  And when I asked to refine the nose, it lost the character and just gave me a kind of dog or fox</p>

<p><a href="/content/images/2026/04/mai-04.png"><img src="/content/images/2026/04/mai-04.png" alt="/content/images/2026/04/mai-04.png" /></a></p>

<p>These same prompts work just fine with MJ</p>

<p><a href="/content/images/2026/04/mai-05.png"><img src="/content/images/2026/04/mai-05.png" alt="/content/images/2026/04/mai-05.png" /></a></p>

<p>I tried dumping the full descriptive writeup from Gemini into MJ to see what it would return, strange but close</p>

<p><a href="/content/images/2026/04/mj-06.png"><img src="/content/images/2026/04/mj-06.png" alt="/content/images/2026/04/mj-06.png" /></a></p>

<p>I think it’s interesting Microsoft even has an AI image play now, but I cannot think what it would be useful for.</p>

<h2 id="diagrams">Diagrams</h2>

<p>Let’s just compare how well these do diagrams from a prompt</p>

<p>With MAI I get this:</p>

<p><a href="/content/images/2026/04/mai-07.png"><img src="/content/images/2026/04/mai-07.png" alt="/content/images/2026/04/mai-07.png" /></a></p>

<p>Midjourney is just odd</p>

<p><a href="/content/images/2026/04/mj-08.png"><img src="/content/images/2026/04/mj-08.png" alt="/content/images/2026/04/mj-08.png" /></a></p>

<p>and Google’s NanoBanana 2 rocks it like a champ</p>

<p><a href="/content/images/2026/04/nanobanana-01.png"><img src="/content/images/2026/04/nanobanana-01.png" alt="/content/images/2026/04/nanobanana-01.png" /></a></p>

<p>I think one could use this almost verbatim in a deck. The subnets are right and it even set correct IPv4s with proper CIDR notations throughout</p>

<p><a href="/content/images/2026/04/nanobanana-02.png"><img src="/content/images/2026/04/nanobanana-02.png" alt="/content/images/2026/04/nanobanana-02.png" /></a></p>

<p>I fired this at Bing (Copilot) and was just curious if it would fair any better.</p>

<p><a href="/content/images/2026/04/copilot-01.png"><img src="/content/images/2026/04/copilot-01.png" alt="/content/images/2026/04/copilot-01.png" /></a></p>

<p>Honestly, that is pretty darn good.</p>

<p><a href="/content/images/2026/04/Copilot_20260401_150055.png"><img src="/content/images/2026/04/Copilot_20260401_150055.png" alt="/content/images/2026/04/Copilot_20260401_150055.png" /></a></p>

<p>I asked it to pivot to GCP and, well it is right, but does look like a Microsoft product made the diagram</p>

<p><a href="/content/images/2026/04/mai-08.png"><img src="/content/images/2026/04/mai-08.png" alt="/content/images/2026/04/mai-08.png" /></a></p>

<h2 id="lettering">Lettering</h2>

<p>What about title slides?  I wanted a Spring-themed “Hello World” so I asked all 4 tools the same prompt:</p>

<blockquote>
  <p>“In block letters on white background “Hello World” with a gentle artistic notes of grass and flower as if the letters were on a spring forest glen. light tones, watercolor”</p>
</blockquote>

<p>Copilot via Edge returned:</p>

<p><a href="/content/images/2026/04/copilot-02.png"><img src="/content/images/2026/04/copilot-02.png" alt="/content/images/2026/04/copilot-02.png" /></a></p>

<p>Nanobanana2 via Gemini showed me:</p>

<p><a href="/content/images/2026/04/nanobanana-03.png"><img src="/content/images/2026/04/nanobanana-03.png" alt="/content/images/2026/04/nanobanana-03.png" /></a></p>

<p>Midjourney gave me more font options, but also more misspellings</p>

<p><a href="/content/images/2026/04/mj-09.png"><img src="/content/images/2026/04/mj-09.png" alt="/content/images/2026/04/mj-09.png" /></a></p>

<p>MAI timed out twice then returned a refusal</p>

<p><a href="/content/images/2026/04/mai-09.png"><img src="/content/images/2026/04/mai-09.png" alt="/content/images/2026/04/mai-09.png" /></a></p>

<p>A fourth attempt, however, worked</p>

<p><a href="/content/images/2026/04/mai-10.png"><img src="/content/images/2026/04/mai-10.png" alt="/content/images/2026/04/mai-10.png" /></a></p>

<h2 id="horror-images">Horror Images</h2>

<p>Another issue I have is the ‘nanny’ filters on everything.  Let’s look at an image from MJ someone created with:</p>

<blockquote>
  <p>Photorealistic painting of a Victorian woman with the upper half of her head wrapped in bandages. The lower half of her face reveals a mouth full of monstrous teeth that are falling out. Numerous other teeth hang from above on threads. Red liquid stains around mouth. Insanely intricately detailed white fabric clothing decorated with teeth. Dramatic lighting. Esoteric, horror, creepy, unsettling, disorienting, dreamcore. Surreal</p>
</blockquote>

<video muted="" controls="">
    <source src="/content/images/2026/04/mj-10.mp4" type="video/mp4" />
</video>

<p>MAI just wont do it</p>

<p><a href="/content/images/2026/04/mai-11.png"><img src="/content/images/2026/04/mai-11.png" alt="/content/images/2026/04/mai-11.png" /></a></p>

<p>Same with Flux via Azure Portal</p>

<p><a href="/content/images/2026/04/flux-01.png"><img src="/content/images/2026/04/flux-01.png" alt="/content/images/2026/04/flux-01.png" /></a></p>

<p>But Copilot would</p>

<p><a href="/content/images/2026/04/copilot-03.png"><img src="/content/images/2026/04/copilot-03.png" alt="/content/images/2026/04/copilot-03.png" /></a></p>

<p>And Nanobanana2 did with Gemini (but only on second attempt)</p>

<p><a href="/content/images/2026/04/nanobanana-04.png"><img src="/content/images/2026/04/nanobanana-04.png" alt="/content/images/2026/04/nanobanana-04.png" /></a></p>

<p>For the record, I even have Flux in the Azure Portal set to the most permissive filters:</p>

<p><a href="/content/images/2026/04/flux-02.png"><img src="/content/images/2026/04/flux-02.png" alt="/content/images/2026/04/flux-02.png" /></a></p>

<h2 id="nice-images">Nice images</h2>

<p>But maybe we need “Nice”</p>

<p>Here is another prompt from MJ:</p>

<blockquote>
  <p>aesthetic spring playlist scene, a cozy outdoor cafe table under full blooming cherry blossom trees, pink petals gently falling, a smartphone with earphones placed on a wooden table, iced coffee with condensation, warm sunlight shining softly, soft shadow, dreamy and romantic mood, pastel color tones, ultra realistic, cinematic lighting, shallow depth of field, natural composition, Korean spring vibe, 50mm lens, high detail, bokeh background</p>
</blockquote>

<video muted="" controls="">
    <source src="/content/images/2026/04/mj-11.mp4" type="video/mp4" />
</video>

<p>Here is Flux via Azure (this, btw, I pay for - not much, but a few cents an image)</p>

<p><a href="/content/images/2026/04/flux-03.png"><img src="/content/images/2026/04/flux-03.png" alt="/content/images/2026/04/flux-03.png" /></a></p>

<p>Copilot rejected it as unsafe, twice - still cannot figure out why</p>

<p><a href="/content/images/2026/04/copilot-04.png"><img src="/content/images/2026/04/copilot-04.png" alt="/content/images/2026/04/copilot-04.png" /></a></p>

<p>However, if I picked “Think Deeper”, it was okay:</p>

<p><a href="/content/images/2026/04/copilot-05.png"><img src="/content/images/2026/04/copilot-05.png" alt="/content/images/2026/04/copilot-05.png" /></a></p>

<p>Nanobanana2 did a pretty good job</p>

<p><a href="/content/images/2026/04/nanobanana-05.png"><img src="/content/images/2026/04/nanobanana-05.png" alt="/content/images/2026/04/nanobanana-05.png" /></a></p>

<p>I even was curious how the video might compare to MJ’s so I then asked it to make a video from the image (with no prompt, just the image)</p>

<video controls="">
    <source src="/content/images/2026/04/veo-01.mp4" type="video/mp4" />
</video>

<p>MAI timed out and refused</p>

<p><a href="/content/images/2026/04/mai-12.png"><img src="/content/images/2026/04/mai-12.png" alt="/content/images/2026/04/mai-12.png" /></a></p>

<p>But on a subsequent try worked</p>

<p><a href="/content/images/2026/04/mai-13.png"><img src="/content/images/2026/04/mai-13.png" alt="/content/images/2026/04/mai-13.png" /></a></p>

<p>it’s a nice image:</p>

<p><a href="/content/images/2026/04/mai-14.png"><img src="/content/images/2026/04/mai-14.png" alt="/content/images/2026/04/mai-14.png" /></a></p>

<h3 id="animating-mai-outputs">Animating MAI outputs</h3>

<p>What if we put that image, but with the original prompt into MJ for animation?</p>

<p><a href="/content/images/2026/04/mj-12.png"><img src="/content/images/2026/04/mj-12.png" alt="/content/images/2026/04/mj-12.png" /></a></p>

<video controls="">
    <source src="/content/images/2026/04/mj-13.mp4" type="video/mp4" />
</video>

<p>And Gemini (for VEO2)?</p>

<video controls="">
    <source src="/content/images/2026/04/veo-02.mp4" type="video/mp4" />
</video>

<h1 id="summary">Summary</h1>

<p>My goal was to look at Microsoft AI Playground, aka <a href="https://playground.microsoft.ai/chat">MAI</a>.  Today it just does images, but says voices are coming soon.</p>

<p>There is no cost to it, so that is nice, but I struggle with “why?” in 2026. Perhaps this is just Microsoft’s first foray into non-OpenAI models?</p>

<p>To put it through it’s paces, we compared its output to that which we can get with Google’s ‘Nano Banana 2’ via <a href="https://gemini.google.com/">Gemini portal</a>, <a href="https://www.midjourney.com/">MidJourney</a>, and <a href="https://copilot.microsoft.com/">Copilot</a> - which I used the edge browser side bar, but I think that URL does the same.</p>

<p>I was interested in copyrighted art (e.g ALF), diagrams, and block lettering.</p>

<p>Overall I found MAI slow, and often rejecting my prompts.  I found I got better results in all the other tools.  I even tried using MS Paint (which yes, just uses Copilot)</p>

<p><a href="/content/images/2026/04/mspaint-01.png"><img src="/content/images/2026/04/mspaint-01.png" alt="/content/images/2026/04/mspaint-01.png" /></a></p>

<p>Part of me thinks MAI is too little too late, but another is intrigued enough to come back later and see what is new.</p>

<p>For now, between content filters, speed, and general usability, I’ll likely stick to my current tools (MJ and Gemini)</p>

<p>At the end of writing this, I wanted to use MAI for its own banner but was at this point blocked for a few hours</p>

<p><a href="/content/images/2026/04/mai-15.png"><img src="/content/images/2026/04/mai-15.png" alt="/content/images/2026/04/mai-15.png" /></a></p>]]></content><author><name>Isaac Johnson</name></author><category term="GenAI" /><category term="MAI" /><category term="Gemini" /><category term="Nanobanana" /><category term="Flux" /><category term="Copilot" /><summary type="html"><![CDATA[Microsoft recently rolled out MAI, a Microsoft AI playground that can do images. Just images. I learned about it from this Decrypt article. I wanted to try it and compare its performance and output to some of the other tools I use often; Midjourney, Gemini (Nanabanana 2) and Copilot itself.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/23bba4971d72675f.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/23bba4971d72675f.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Gemma4, Claude and Pi</title><link href="https://freshbrewed.science/2026/04/09/gemma.html" rel="alternate" type="text/html" title="Gemma4, Claude and Pi" /><published>2026-04-09T01:01:01+00:00</published><updated>2026-04-09T01:01:01+00:00</updated><id>https://freshbrewed.science/2026/04/09/gemma</id><content type="html" xml:base="https://freshbrewed.science/2026/04/09/gemma.html"><![CDATA[<p>I had two things I wanted to tackle today: Comparing using Anthropic via <a href="https://github.com/badlogic/pi-mono">Pi CLI</a> versus Claude Code, then taking a look at Gemma4 - the new open model Google released last week.  In this article, I want to dig into <a href="https://ollama.com/library/gemma4">Gemma4</a> more thoroughly rather than rush an article just to get on the headline bandwagon.</p>

<p>As I’ll get into below, the first section (Pi vs Claude) came from a user request via <a href="https://www.linkedin.com/posts/isaacinmn_pi-coding-agent-share-7444716879790837760-kAya?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAAkg5wB9p5ZPIZqcc0aVS3My9TP21c1oQQ">LinkedIn</a>.  I tend to avoid Anthropic just due to price.</p>

<h1 id="anthropic-cli-vs-pi">Anthropic CLI vs Pi</h1>

<p><a href="https://www.linkedin.com/in/samhegge/">Sam Hegge</a> asked me if I could compare Claude Code versus Pi CLI as a comment on my last <a href="https://www.linkedin.com/posts/isaacinmn_pi-coding-agent-share-7444716879790837760-kAya?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAAkg5wB9p5ZPIZqcc0aVS3My9TP21c1oQQ">LI post</a> which was <a href="https://freshbrewed.science/2026/03/26/piagent.html">about the Pi agent</a> how it compared to Claude CLI.</p>

<p>I focus on Gemini CLI mostly because I’m generally Google-first, but also because Gemini AI Pro is covered and Anthropic usage tends to cost me.</p>

<p>That said, it’s a fair question - How might Pi work compared to Claude?  I’m willing to feed AI some coins to see.</p>

<h2 id="anthropic-setup-with-pi">Anthropic setup with Pi</h2>

<p>You just need to set an Anthropic key in your environment for Pi to pick up:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ export ANTHROPIC_API_KEY=sk-ant-api03-X-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
$ pi
</code></pre></div></div>

<p>Now when I ask for models, we can see the latest Anthropic ones listed:</p>

<p><a href="/content/images/2026/04/pi-01.png"><img src="/content/images/2026/04/pi-01.png" alt="/content/images/2026/04/pi-01.png" /></a></p>

<p>I picked the latest <code class="language-plaintext highlighter-rouge">claude-3-7-sonnet-latest</code> to use.</p>

<p>I actually have a desire to make some skills today, so I will actually use a Skill builder Skill for this activity.</p>

<p>I found a good one from Anthropic here: <a href="https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md">https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md</a></p>

<p>I actually already keep a local clone of Anthropic’s primary Skills library, so I just need to update it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/skills/skills$ ls
algorithmic-art   canvas-design    docx             internal-comms  pdf   skill-creator      theme-factory          webapp-testing
brand-guidelines  doc-coauthoring  frontend-design  mcp-builder     pptx  slack-gif-creator  web-artifacts-builder  xlsx
builder@DESKTOP-QADGF36:~/Workspaces/skills/skills$ cd ..
builder@DESKTOP-QADGF36:~/Workspaces/skills$ git remote show origin
* remote origin
  Fetch URL: https://github.com/anthropics/skills.git
  Push  URL: https://github.com/anthropics/skills.git
  HEAD branch: main
  Remote branches:
    andibrae/create-top-level-namespace tracked
    klazuka/add-3p-notices              tracked
    klazuka/add-cc-instructions         tracked
    klazuka/add-cc-marketplace          tracked
    klazuka/doc-skills                  tracked
    klazuka/export                      tracked
    klazuka/export-20260203             new (next fetch will store in remotes/origin)
    klazuka/frontend-design-skill       tracked
    klazuka/pptx-cleanup                new (next fetch will store in remotes/origin)
    klazuka/spec                        tracked
    mahesh/add-to-readme                tracked
    mahesh/clarify-claude-code-install  tracked
    main                                tracked
    mattpic-ant/blog-small-fix          tracked
  Local branch configured for 'git pull':
    main merges with remote main
  Local ref configured for 'git push':
    main pushes to main (local out of date)
builder@DESKTOP-QADGF36:~/Workspaces/skills$ git pull
remote: Enumerating objects: 226, done.
remote: Counting objects: 100% (12/12), done.
remote: Total 226 (delta 12), reused 12 (delta 12), pack-reused 214 (from 1)
Receiving objects: 100% (226/226), 293.56 KiB | 2.39 MiB/s, done.
Resolving deltas: 100% (60/60), completed with 5 local objects.
From https://github.com/anthropics/skills
   69c0b1a..98669c1  main                    -&gt; origin/main
 * [new branch]      klazuka/export-20260203 -&gt; origin/klazuka/export-20260203
 * [new branch]      klazuka/pptx-cleanup    -&gt; origin/klazuka/pptx-cleanup
Updating 69c0b1a..98669c1
Fast-forward
 .claude-plugin/marketplace.json                                                             |   10 +
 skills/claude-api/LICENSE.txt                                                               |  202 ++
 skills/claude-api/SKILL.md                                                                  |  262 +++
 skills/claude-api/csharp/claude-api.md                                                      |  402 ++++
 skills/claude-api/curl/examples.md                                                          |  216 +++
 skills/claude-api/go/claude-api.md                                                          |  421 +++++
 skills/claude-api/java/claude-api.md                                                        |  432 +++++
 skills/claude-api/php/claude-api.md                                                         |  375 ++++
 skills/claude-api/python/agent-sdk/README.md                                                |  355 ++++
 skills/claude-api/python/agent-sdk/patterns.md                                              |  359 ++++
 skills/claude-api/python/claude-api/README.md                                               |  420 +++++
 skills/claude-api/python/claude-api/batches.md                                              |  185 ++
 skills/claude-api/python/claude-api/files-api.md                                            |  165 ++
 skills/claude-api/python/claude-api/streaming.md                                            |  162 ++
 skills/claude-api/python/claude-api/tool-use.md                                             |  590 ++++++
 skills/claude-api/ruby/claude-api.md                                                        |  113 ++
 skills/claude-api/shared/error-codes.md                                                     |  206 ++
 skills/claude-api/shared/live-sources.md                                                    |  121 ++
 skills/claude-api/shared/models.md                                                          |  119 ++
 skills/claude-api/shared/prompt-caching.md                                                  |  128 ++
 skills/claude-api/shared/tool-use-concepts.md                                               |  305 +++
 skills/claude-api/typescript/agent-sdk/README.md                                            |  297 +++
 skills/claude-api/typescript/agent-sdk/patterns.md                                          |  209 +++
 skills/claude-api/typescript/claude-api/README.md                                           |  333 ++++
 skills/claude-api/typescript/claude-api/batches.md                                          |  106 ++
 skills/claude-api/typescript/claude-api/files-api.md                                        |   98 +
 skills/claude-api/typescript/claude-api/streaming.md                                        |  178 ++
 skills/claude-api/typescript/claude-api/tool-use.md                                         |  527 ++++++
 skills/docx/SKILL.md                                                                        |  659 +++++--
 skills/docx/docx-js.md                                                                      |  350 ----
 skills/docx/ooxml.md                                                                        |  610 ------
 skills/docx/ooxml/scripts/pack.py                                                           |  159 --
 skills/docx/ooxml/scripts/unpack.py                                                         |   29 -
 skills/docx/ooxml/scripts/validate.py                                                       |   69 -
 skills/docx/ooxml/scripts/validation/docx.py                                                |  274 ---
 skills/docx/scripts/__init__.py                                                             |    2 +-
 skills/docx/scripts/accept_changes.py                                                       |  135 ++
 skills/docx/scripts/comment.py                                                              |  318 ++++
 skills/docx/scripts/document.py                                                             | 1276 -------------
 skills/docx/scripts/office/helpers/__init__.py                                              |    0
 skills/docx/scripts/office/helpers/merge_runs.py                                            |  199 ++
 skills/docx/scripts/office/helpers/simplify_redlines.py                                     |  197 ++
 skills/docx/scripts/office/pack.py                                                          |  159 ++
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd             |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd      |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd           |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd      |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd              |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd           |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd     |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd                   |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd                |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd   |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd                  |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd                |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd                 |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd               |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd           |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd           |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd  |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd                   |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd              |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd     |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd  |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd     |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd                   |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd                   |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd       |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd     |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd             |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd      |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd                                    |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd                        |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd                        |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd                        |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd                    |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd                    |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd            |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd                  |    0
 skills/docx/scripts/office/soffice.py                                                       |  183 ++
 skills/docx/scripts/office/unpack.py                                                        |  132 ++
 skills/docx/scripts/office/validate.py                                                      |  111 ++
 skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py             |    0
 skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/base.py            |  310 +---
 skills/docx/scripts/office/validators/docx.py                                               |  446 +++++
 skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/pptx.py            |   44 +-
 skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py            |   74 +-
 skills/docx/scripts/templates/comments.xml                                                  |    4 +-
 skills/docx/scripts/templates/commentsExtended.xml                                          |    4 +-
 skills/docx/scripts/templates/commentsExtensible.xml                                        |    4 +-
 skills/docx/scripts/templates/commentsIds.xml                                               |    4 +-
 skills/docx/scripts/templates/people.xml                                                    |    4 +-
 skills/docx/scripts/utilities.py                                                            |  374 ----
 skills/pdf/SKILL.md                                                                         |   34 +-
 skills/pdf/forms.md                                                                         |  283 ++-
 skills/pdf/scripts/check_bounding_boxes.py                                                  |    5 -
 skills/pdf/scripts/check_bounding_boxes_test.py                                             |  226 ---
 skills/pdf/scripts/check_fillable_fields.py                                                 |    1 -
 skills/pdf/scripts/convert_pdf_to_images.py                                                 |    2 -
 skills/pdf/scripts/create_validation_image.py                                               |    4 -
 skills/pdf/scripts/extract_form_field_info.py                                               |   32 +-
 skills/pdf/scripts/extract_form_structure.py                                                |  115 ++
 skills/pdf/scripts/fill_fillable_fields.py                                                  |   16 -
 skills/pdf/scripts/fill_pdf_form_with_annotations.py                                        |   61 +-
 skills/pptx/SKILL.md                                                                        |  652 ++-----
 skills/pptx/editing.md                                                                      |  205 ++
 skills/pptx/html2pptx.md                                                                    |  625 -------
 skills/pptx/ooxml.md                                                                        |  427 -----
 skills/pptx/ooxml/scripts/pack.py                                                           |  159 --
 skills/pptx/ooxml/scripts/unpack.py                                                         |   29 -
 skills/pptx/ooxml/scripts/validate.py                                                       |   69 -
 skills/pptx/ooxml/scripts/validation/docx.py                                                |  274 ---
 skills/pptx/pptxgenjs.md                                                                    |  420 +++++
 skills/pptx/scripts/__init__.py                                                             |    0
 skills/pptx/scripts/add_slide.py                                                            |  195 ++
 skills/pptx/scripts/clean.py                                                                |  286 +++
 skills/pptx/scripts/html2pptx.js                                                            |  979 ----------
 skills/pptx/scripts/inventory.py                                                            | 1020 ----------
 skills/pptx/scripts/office/helpers/__init__.py                                              |    0
 skills/pptx/scripts/office/helpers/merge_runs.py                                            |  199 ++
 skills/pptx/scripts/office/helpers/simplify_redlines.py                                     |  197 ++
 skills/pptx/scripts/office/pack.py                                                          |  159 ++
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd             |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd      |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd           |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd      |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd              |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd           |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd     |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd                   |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd                |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd   |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd                  |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd                |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd                 |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd               |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd           |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd           |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd  |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd                   |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd              |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd     |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd  |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd     |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd                   |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd                   |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd       |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd     |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd             |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd      |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd                                    |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd                        |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd                        |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd                        |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd                    |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd                    |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd            |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd                  |    0
 skills/pptx/scripts/office/soffice.py                                                       |  183 ++
 skills/pptx/scripts/office/unpack.py                                                        |  132 ++
 skills/pptx/scripts/office/validate.py                                                      |  111 ++
 skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py             |    0
 skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/base.py            |  310 +---
 skills/pptx/scripts/office/validators/docx.py                                               |  446 +++++
 skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/pptx.py            |   44 +-
 skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py            |   74 +-
 skills/pptx/scripts/rearrange.py                                                            |  231 ---
 skills/pptx/scripts/replace.py                                                              |  385 ----
 skills/pptx/scripts/thumbnail.py                                                            |  399 ++--
 skills/skill-creator/SKILL.md                                                               |  555 +++---
 skills/skill-creator/agents/analyzer.md                                                     |  274 +++
 skills/skill-creator/agents/comparator.md                                                   |  202 ++
 skills/skill-creator/agents/grader.md                                                       |  223 +++
 skills/skill-creator/assets/eval_review.html                                                |  146 ++
 skills/skill-creator/eval-viewer/generate_review.py                                         |  471 +++++
 skills/skill-creator/eval-viewer/viewer.html                                                | 1325 +++++++++++++
 skills/skill-creator/references/output-patterns.md                                          |   82 -
 skills/skill-creator/references/schemas.md                                                  |  430 +++++
 skills/skill-creator/references/workflows.md                                                |   28 -
 skills/skill-creator/scripts/__init__.py                                                    |    0
 skills/skill-creator/scripts/aggregate_benchmark.py                                         |  401 ++++
 skills/skill-creator/scripts/generate_report.py                                             |  326 ++++
 skills/skill-creator/scripts/improve_description.py                                         |  247 +++
 skills/skill-creator/scripts/init_skill.py                                                  |  303 ---
 skills/skill-creator/scripts/package_skill.py                                               |   40 +-
 skills/skill-creator/scripts/quick_validate.py                                              |   14 +-
 skills/skill-creator/scripts/run_eval.py                                                    |  310 ++++
 skills/skill-creator/scripts/run_loop.py                                                    |  328 ++++
 skills/skill-creator/scripts/utils.py                                                       |   47 +
 skills/xlsx/SKILL.md                                                                        |   21 +-
 skills/xlsx/recalc.py                                                                       |  178 --
 skills/xlsx/scripts/office/helpers/__init__.py                                              |    0
 skills/xlsx/scripts/office/helpers/merge_runs.py                                            |  199 ++
 skills/xlsx/scripts/office/helpers/simplify_redlines.py                                     |  197 ++
 skills/xlsx/scripts/office/pack.py                                                          |  159 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd                        | 1499 +++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd                 |  146 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd                      | 1085 +++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd                 |   11 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd                         | 3081 ++++++++++++++++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd                      |   23 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd           |  185 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd        |  287 +++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd                              | 1676 +++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd |   28 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd              |  144 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd         |  174 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd   |   25 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd |   18 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd  |   59 +
 .../xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd   |   56 +
 .../scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd    |  195 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd                      |  582 ++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd     |   25 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd                              | 4439 ++++++++++++++++++++++++++++++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd                         |  570 ++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd                |  509 +++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd          |   12 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd           |  108 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd        |   96 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd                              | 3646 ++++++++++++++++++++++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd                              |  116 ++
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd                  |   42 +
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd                |   50 +
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd                        |   49 +
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd                 |   33 +
 skills/xlsx/scripts/office/schemas/mce/mc.xsd                                               |   75 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd                                   |  560 ++++++
 skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd                                   |   67 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd                                   |   14 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd                               |   20 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd                               |   13 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd                       |    4 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd                             |    8 +
 skills/xlsx/scripts/office/soffice.py                                                       |  183 ++
 skills/xlsx/scripts/office/unpack.py                                                        |  132 ++
 skills/xlsx/scripts/office/validate.py                                                      |  111 ++
 skills/xlsx/scripts/office/validators/__init__.py                                           |   15 +
 skills/xlsx/scripts/office/validators/base.py                                               |  847 +++++++++
 skills/xlsx/scripts/office/validators/docx.py                                               |  446 +++++
 skills/xlsx/scripts/office/validators/pptx.py                                               |  275 +++
 skills/xlsx/scripts/office/validators/redlining.py                                          |  247 +++
 skills/xlsx/scripts/recalc.py                                                               |  184 ++
 249 files changed, 41029 insertions(+), 10062 deletions(-)
 create mode 100644 skills/claude-api/LICENSE.txt
 create mode 100644 skills/claude-api/SKILL.md
 create mode 100644 skills/claude-api/csharp/claude-api.md
 create mode 100644 skills/claude-api/curl/examples.md
 create mode 100644 skills/claude-api/go/claude-api.md
 create mode 100644 skills/claude-api/java/claude-api.md
 create mode 100644 skills/claude-api/php/claude-api.md
 create mode 100644 skills/claude-api/python/agent-sdk/README.md
 create mode 100644 skills/claude-api/python/agent-sdk/patterns.md
 create mode 100644 skills/claude-api/python/claude-api/README.md
 create mode 100644 skills/claude-api/python/claude-api/batches.md
 create mode 100644 skills/claude-api/python/claude-api/files-api.md
 create mode 100644 skills/claude-api/python/claude-api/streaming.md
 create mode 100644 skills/claude-api/python/claude-api/tool-use.md
 create mode 100644 skills/claude-api/ruby/claude-api.md
 create mode 100644 skills/claude-api/shared/error-codes.md
 create mode 100644 skills/claude-api/shared/live-sources.md
 create mode 100644 skills/claude-api/shared/models.md
 create mode 100644 skills/claude-api/shared/prompt-caching.md
 create mode 100644 skills/claude-api/shared/tool-use-concepts.md
 create mode 100644 skills/claude-api/typescript/agent-sdk/README.md
 create mode 100644 skills/claude-api/typescript/agent-sdk/patterns.md
 create mode 100644 skills/claude-api/typescript/claude-api/README.md
 create mode 100644 skills/claude-api/typescript/claude-api/batches.md
 create mode 100644 skills/claude-api/typescript/claude-api/files-api.md
 create mode 100644 skills/claude-api/typescript/claude-api/streaming.md
 create mode 100644 skills/claude-api/typescript/claude-api/tool-use.md
 delete mode 100644 skills/docx/docx-js.md
 delete mode 100644 skills/docx/ooxml.md
 delete mode 100755 skills/docx/ooxml/scripts/pack.py
 delete mode 100755 skills/docx/ooxml/scripts/unpack.py
 delete mode 100755 skills/docx/ooxml/scripts/validate.py
 delete mode 100644 skills/docx/ooxml/scripts/validation/docx.py
 create mode 100755 skills/docx/scripts/accept_changes.py
 create mode 100755 skills/docx/scripts/comment.py
 delete mode 100755 skills/docx/scripts/document.py
 create mode 100644 skills/docx/scripts/office/helpers/__init__.py
 create mode 100644 skills/docx/scripts/office/helpers/merge_runs.py
 create mode 100644 skills/docx/scripts/office/helpers/simplify_redlines.py
 create mode 100755 skills/docx/scripts/office/pack.py
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd (100%)
 create mode 100644 skills/docx/scripts/office/soffice.py
 create mode 100755 skills/docx/scripts/office/unpack.py
 create mode 100755 skills/docx/scripts/office/validate.py
 rename skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py (100%)
 rename skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/base.py (71%)
 create mode 100644 skills/docx/scripts/office/validators/docx.py
 rename skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/pptx.py (79%)
 rename skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py (72%)
 delete mode 100755 skills/docx/scripts/utilities.py
 delete mode 100644 skills/pdf/scripts/check_bounding_boxes_test.py
 create mode 100755 skills/pdf/scripts/extract_form_structure.py
 create mode 100644 skills/pptx/editing.md
 delete mode 100644 skills/pptx/html2pptx.md
 delete mode 100644 skills/pptx/ooxml.md
 delete mode 100755 skills/pptx/ooxml/scripts/pack.py
 delete mode 100755 skills/pptx/ooxml/scripts/unpack.py
 delete mode 100755 skills/pptx/ooxml/scripts/validate.py
 delete mode 100644 skills/pptx/ooxml/scripts/validation/docx.py
 create mode 100644 skills/pptx/pptxgenjs.md
 create mode 100644 skills/pptx/scripts/__init__.py
 create mode 100755 skills/pptx/scripts/add_slide.py
 create mode 100755 skills/pptx/scripts/clean.py
 delete mode 100755 skills/pptx/scripts/html2pptx.js
 delete mode 100755 skills/pptx/scripts/inventory.py
 create mode 100644 skills/pptx/scripts/office/helpers/__init__.py
 create mode 100644 skills/pptx/scripts/office/helpers/merge_runs.py
 create mode 100644 skills/pptx/scripts/office/helpers/simplify_redlines.py
 create mode 100755 skills/pptx/scripts/office/pack.py
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd (100%)
 create mode 100644 skills/pptx/scripts/office/soffice.py
 create mode 100755 skills/pptx/scripts/office/unpack.py
 create mode 100755 skills/pptx/scripts/office/validate.py
 rename skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py (100%)
 rename skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/base.py (71%)
 create mode 100644 skills/pptx/scripts/office/validators/docx.py
 rename skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/pptx.py (79%)
 rename skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py (72%)
 delete mode 100755 skills/pptx/scripts/rearrange.py
 delete mode 100755 skills/pptx/scripts/replace.py
 create mode 100644 skills/skill-creator/agents/analyzer.md
 create mode 100644 skills/skill-creator/agents/comparator.md
 create mode 100644 skills/skill-creator/agents/grader.md
 create mode 100644 skills/skill-creator/assets/eval_review.html
 create mode 100644 skills/skill-creator/eval-viewer/generate_review.py
 create mode 100644 skills/skill-creator/eval-viewer/viewer.html
 delete mode 100644 skills/skill-creator/references/output-patterns.md
 create mode 100644 skills/skill-creator/references/schemas.md
 delete mode 100644 skills/skill-creator/references/workflows.md
 create mode 100644 skills/skill-creator/scripts/__init__.py
 create mode 100755 skills/skill-creator/scripts/aggregate_benchmark.py
 create mode 100755 skills/skill-creator/scripts/generate_report.py
 create mode 100755 skills/skill-creator/scripts/improve_description.py
 delete mode 100755 skills/skill-creator/scripts/init_skill.py
 create mode 100755 skills/skill-creator/scripts/run_eval.py
 create mode 100755 skills/skill-creator/scripts/run_loop.py
 create mode 100644 skills/skill-creator/scripts/utils.py
 delete mode 100644 skills/xlsx/recalc.py
 create mode 100644 skills/xlsx/scripts/office/helpers/__init__.py
 create mode 100644 skills/xlsx/scripts/office/helpers/merge_runs.py
 create mode 100644 skills/xlsx/scripts/office/helpers/simplify_redlines.py
 create mode 100755 skills/xlsx/scripts/office/pack.py
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/mce/mc.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd
 create mode 100644 skills/xlsx/scripts/office/soffice.py
 create mode 100755 skills/xlsx/scripts/office/unpack.py
 create mode 100755 skills/xlsx/scripts/office/validate.py
 create mode 100644 skills/xlsx/scripts/office/validators/__init__.py
 create mode 100644 skills/xlsx/scripts/office/validators/base.py
 create mode 100644 skills/xlsx/scripts/office/validators/docx.py
 create mode 100644 skills/xlsx/scripts/office/validators/pptx.py
 create mode 100644 skills/xlsx/scripts/office/validators/redlining.py
 create mode 100755 skills/xlsx/scripts/recalc.py
builder@DESKTOP-QADGF36:~/Workspaces/skills$ ls
README.md  THIRD_PARTY_NOTICES.md  skills  spec  template
builder@DESKTOP-QADGF36:~/Workspaces/skills$ ls skills/
algorithmic-art   canvas-design  doc-coauthoring  frontend-design  mcp-builder  pptx           slack-gif-creator  web-artifacts-builder  xlsx
brand-guidelines  claude-api     docx             internal-comms   pdf          skill-creator  theme-factory      webapp-testing
</code></pre></div></div>

<p>I noticed since I updated last, they removed <code class="language-plaintext highlighter-rouge">claude-api</code>.  Wonder if the leaks of their code had anything to do with that.</p>

<p>My mode of using skills is to sym-link or copy them in the more global <code class="language-plaintext highlighter-rouge">~/.agent/skills</code></p>

<p>I’ll go ahead and link that <code class="language-plaintext highlighter-rouge">skill creator</code> there</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ln -s /home/builder/Workspaces/skills/skills/skill-creator /home/builder/.agents/skills/skill-creator
</code></pre></div></div>

<p>My second habit is to aggregate my newly created skills into their own private GIT repo</p>

<p><a href="/content/images/2026/04/pi-02.png"><img src="/content/images/2026/04/pi-02.png" alt="/content/images/2026/04/pi-02.png" /></a></p>

<p>Since I actually care about these now, I’ll do a backup to Codeberg (always good to do 2 homes for GIT)</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/forgejo-01.mp4" type="video/mp4" />
</video>

<p>Now that that is sorted, let’s clone our own agentskills repo to build something</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://forgejo.freshbrewed.science/builderadmin/agentskills.git
Cloning into 'agentskills'...
remote: Enumerating objects: 11, done.
remote: Counting objects: 100% (11/11), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 11 (delta 1), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (11/11), 4.07 KiB | 4.07 MiB/s, done.
Resolving deltas: 100% (1/1), done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd agentskills/
builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ nvm use lts/jod
Now using node v22.22.0 (npm v10.9.4)
</code></pre></div></div>

<p>I fired up Pi and tried to load my skill.. seems i need to feed the pig…</p>

<p><a href="/content/images/2026/04/pi-03.png"><img src="/content/images/2026/04/pi-03.png" alt="/content/images/2026/04/pi-03.png" /></a></p>

<p>I have auto-reload disabled so when it’s out it’s out. So i fed it a $20</p>

<p><a href="/content/images/2026/04/pi-04.png"><img src="/content/images/2026/04/pi-04.png" alt="/content/images/2026/04/pi-04.png" /></a></p>

<p>Anthropic also does this use it or lose it thing</p>

<p><a href="/content/images/2026/04/pi-05.png"><img src="/content/images/2026/04/pi-05.png" alt="/content/images/2026/04/pi-05.png" /></a></p>

<p>So that’s another reason I’m not a giant fan of them, even if their tooling is pretty good.</p>

<p>My next issue was Sonnet 3.7 isn’t available.. I think my eyes just screwed that up seeing the “.7”.. 4.6 is the latest, not 3.7</p>

<p><a href="/content/images/2026/04/pi-06.png"><img src="/content/images/2026/04/pi-06.png" alt="/content/images/2026/04/pi-06.png" /></a></p>

<p>I can use the Skill creator with “/skill:skill-creator” - this makes Pi definitely load that creator skill for our next step.</p>

<p>Loading the skill already cost me 7c</p>

<p><a href="/content/images/2026/04/pi-07.png"><img src="/content/images/2026/04/pi-07.png" alt="/content/images/2026/04/pi-07.png" /></a></p>

<p>Let’s now build a skill out with Sonnet 4.6</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/pi-08.mp4" type="video/mp4" />
</video>

<p>As we can see, that took about 5 minutes with Pi and cost about US$0.395</p>

<p><a href="/content/images/2026/04/pi-09.png"><img src="/content/images/2026/04/pi-09.png" alt="/content/images/2026/04/pi-09.png" /></a></p>

<h2 id="claude-native">Claude native</h2>

<p>Let’s now fire up Claude natively in the same directory</p>

<p><a href="/content/images/2026/04/claudecode-01.png"><img src="/content/images/2026/04/claudecode-01.png" alt="/content/images/2026/04/claudecode-01.png" /></a></p>

<p>As we can see, it can create a new Helm Chart skill, but it loves to launch agents in Parallel which I fear is going to cost me.</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/claudecode-02.mp4" type="video/mp4" />
</video>

<p>it finished</p>

<p><a href="/content/images/2026/04/claudecode-03.png"><img src="/content/images/2026/04/claudecode-03.png" alt="/content/images/2026/04/claudecode-03.png" /></a></p>

<p>And it did build out a skill</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ tree ./helm-charts-workspace/
./helm-charts-workspace/
└── iteration-1
    ├── benchmark.json
    ├── benchmark.md
    ├── eval-1
    │   ├── eval_metadata.json
    │   ├── with_skill
    │   │   └── run-1
    │   │       ├── grading.json
    │   │       ├── outputs
    │   │       │   ├── Chart.yaml
    │   │       │   ├── templates
    │   │       │   │   ├── NOTES.txt
    │   │       │   │   ├── _helpers.tpl
    │   │       │   │   ├── deployment.yaml
    │   │       │   │   ├── ingress.yaml
    │   │       │   │   ├── namespace.yaml
    │   │       │   │   ├── pvc.yaml
    │   │       │   │   └── service.yaml
    │   │       │   └── values.yaml
    │   │       └── timing.json
    │   └── without_skill
    │       └── run-1
    │           ├── grading.json
    │           ├── outputs
    │           │   ├── Chart.yaml
    │           │   ├── templates
    │           │   │   ├── NOTES.txt
    │           │   │   ├── _helpers.tpl
    │           │   │   ├── deployment.yaml
    │           │   │   ├── ingress.yaml
    │           │   │   ├── namespace.yaml
    │           │   │   ├── pvc.yaml
    │           │   │   └── service.yaml
    │           │   └── values.yaml
    │           └── timing.json
    ├── eval-2
    │   ├── eval_metadata.json
    │   ├── with_skill
    │   │   └── run-1
    │   │       ├── grading.json
    │   │       ├── outputs
    │   │       │   ├── Chart.yaml
    │   │       │   ├── templates
    │   │       │   │   ├── NOTES.txt
    │   │       │   │   ├── _helpers.tpl
    │   │       │   │   ├── deployment.yaml
    │   │       │   │   └── service.yaml
    │   │       │   └── values.yaml
    │   │       └── timing.json
    │   └── without_skill
    │       └── run-1
    │           ├── grading.json
    │           ├── outputs
    │           │   ├── Chart.yaml
    │           │   ├── templates
    │           │   │   ├── NOTES.txt
    │           │   │   ├── _helpers.tpl
    │           │   │   ├── deployment.yaml
    │           │   │   └── service.yaml
    │           │   └── values.yaml
    │           └── timing.json
    └── eval-3
        ├── eval_metadata.json
        ├── with_skill
        │   └── run-1
        │       ├── grading.json
        │       ├── outputs
        │       │   ├── templates
        │       │   │   ├── ingress.yaml
        │       │   │   └── namespace.yaml
        │       │   └── values_ingress_section.yaml
        │       └── timing.json
        └── without_skill
            └── run-1
                ├── grading.json
                ├── outputs
                │   ├── templates
                │   │   ├── ingress.yaml
                │   │   └── namespace.yaml
                │   └── values_ingress_section.yaml
                └── timing.json

28 directories, 53 files
</code></pre></div></div>

<p>But cost me at least $2.30</p>

<p><a href="/content/images/2026/04/claudecode-04.png"><img src="/content/images/2026/04/claudecode-04.png" alt="/content/images/2026/04/claudecode-04.png" /></a></p>

<p>I think that’s why heavy users quickly switch from the free model with pay-per-token to a Claude Pro or Max plan</p>

<p><a href="/content/images/2026/04/claudecode-05.png"><img src="/content/images/2026/04/claudecode-05.png" alt="/content/images/2026/04/claudecode-05.png" /></a></p>

<h1 id="gemma-4">Gemma 4</h1>

<p>I’m on my Windows box at the moment, so let’s pull down Gemma4 which dropped in Ollama just last night</p>

<p><a href="/content/images/2026/04/gemma4-01.png"><img src="/content/images/2026/04/gemma4-01.png" alt="/content/images/2026/04/gemma4-01.png" /></a></p>

<p>While it would appear I could download with my Ollama</p>

<p><a href="/content/images/2026/04/gemma4-02.png"><img src="/content/images/2026/04/gemma4-02.png" alt="/content/images/2026/04/gemma4-02.png" /></a></p>

<p>I’m prompted to upgrade them moment i try and use it</p>

<p><a href="/content/images/2026/04/gemma4-04.png"><img src="/content/images/2026/04/gemma4-04.png" alt="/content/images/2026/04/gemma4-04.png" /></a></p>

<p>In Windows, I don’t know a great way to just tell it to pull a model other than launch from system tray and try and use it.  The first time you do it downloads the model of which you seek</p>

<p><a href="/content/images/2026/04/gemma4-03.png"><img src="/content/images/2026/04/gemma4-03.png" alt="/content/images/2026/04/gemma4-03.png" /></a></p>

<p>Let’s ask a coding question about Perl and see what it comes back with.  I’ll pause a few times, but leave the timer up</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-05.mp4" type="video/mp4" />
</video>

<p>That took about 2 minutes.  This is pretty promising especially because my desktop nVidia 3070 only has 8Gb dedicated memory so it’s not ‘giant’ (my new laptop has 12Gb, but let’s keep with Windows for the moment).</p>

<p>My next test will use Gemma4 in VS Code by way of Continue.dev’s plugin.  My plugin uses JSON, but yours might use YAML</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="err">...</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p><a href="/content/images/2026/04/gemma4-06.png"><img src="/content/images/2026/04/gemma4-06.png" alt="/content/images/2026/04/gemma4-06.png" /></a></p>

<p>I worked through making a service skill</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-06.mp4" type="video/mp4" />
</video>

<p>Many times over, it would get stuck making files and i would nudge it along.  In the end, I just made the files for it and put in the contents.  While i spent roughly 30m on this, I was also distracted and reading some articles so it was more like 15m to get the the work done.</p>

<p>I wanted to see if any of the charts were a bit borked, so I did a local test:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ helm install --dry-run --debug myfakeskill ./helm-chart/
install.go:225: 2026-04-03 09:17:27.184010461 -0500 CDT m=+0.282573365 [debug] Original chart version: ""
install.go:242: 2026-04-03 09:17:27.184974141 -0500 CDT m=+0.283537045 [debug] CHART PATH: /home/builder/Workspaces/agentskills/python-app-setup/helm-chart

Error: INSTALLATION FAILED: template: fastapi-service/templates/secret.yaml:10:21: executing "fastapi-service/templates/secret.yaml" at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue
helm.go:86: 2026-04-03 09:17:27.265751891 -0500 CDT m=+0.364314795 [debug] template: fastapi-service/templates/secret.yaml:10:21: executing "fastapi-service/templates/secret.yaml" at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue
INSTALLATION FAILED
main.newInstallCmd.func2
        helm.sh/helm/v3/cmd/helm/install.go:158
github.com/spf13/cobra.(*Command).execute
        github.com/spf13/cobra@v1.8.1/command.go:985
github.com/spf13/cobra.(*Command).ExecuteC
        github.com/spf13/cobra@v1.8.1/command.go:1117
github.com/spf13/cobra.(*Command).Execute
        github.com/spf13/cobra@v1.8.1/command.go:1041
main.main
        helm.sh/helm/v3/cmd/helm/helm.go:85
runtime.main
        runtime/proc.go:283
runtime.goexit
        runtime/asm_amd64.s:1700
</code></pre></div></div>

<p>Let’s now try to fix this with Pi using our locally hosted models</p>

<p>I’ll create (or update) <code class="language-plaintext highlighter-rouge">~/.pi/agent/models.json</code></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"providers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ollama"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"baseUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.160:11434/v1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"api"</span><span class="p">:</span><span class="w"> </span><span class="s2">"openai-completions"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"apiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"qwen3:latest"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Now, most will use <code class="language-plaintext highlighter-rouge">localhost</code> for the baseURL.  But I found WSL doesn’t “see” my windows hosted Ollama and if i use Ollama in WSL, it doesn’t properly see my nVidia card.  So even tho this is running in WSL on that exact host, I’m using my local IP seen in Windows (192.168.1.160) for the address.</p>

<p><a href="/content/images/2026/04/gemma4-07.png"><img src="/content/images/2026/04/gemma4-07.png" alt="/content/images/2026/04/gemma4-07.png" /></a></p>

<p>Let’s ask Pi (using Gemma4) to fix it</p>

<p><a href="/content/images/2026/04/gemma4-08.png"><img src="/content/images/2026/04/gemma4-08.png" alt="/content/images/2026/04/gemma4-08.png" /></a></p>

<p>It took about 2:40 to come up with it’s first idea for a fix, which required me to confirm (though Pi didn’t really suggest that, i just wrote “yes, do that” and hit enter).</p>

<p>However, over and over it would claim to do an edit but then just sit there. i ran that 3 times</p>

<p><a href="/content/images/2026/04/gemma4-09.png"><img src="/content/images/2026/04/gemma4-09.png" alt="/content/images/2026/04/gemma4-09.png" /></a></p>

<h2 id="opencode">OpenCode</h2>

<p>Like Pi, there is another local CLI we can use with Ollama, <a href="https://opencode.ai/">Opencode</a></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ npm i -g opencode-ai

added 5 packages in 8s
</code></pre></div></div>

<p>I need to create a local opencode json file with models</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ cat opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "ollama": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Ollama (local)",
      "options": {
        "baseURL": "http://192.168.1.160:11434/v1"
      },
      "models": {
        "gemma4:e4b": {
          "name": "gemma4"
        }
      }
    }
  }
}
</code></pre></div></div>

<p>It picked up the Anthropic key so I need to move to the gemma4 model I defined</p>

<p><a href="/content/images/2026/04/opencode-01.png"><img src="/content/images/2026/04/opencode-01.png" alt="/content/images/2026/04/opencode-01.png" /></a></p>

<p>I’ll feed the same prompt</p>

<p><a href="/content/images/2026/04/opencode-02.png"><img src="/content/images/2026/04/opencode-02.png" alt="/content/images/2026/04/opencode-02.png" /></a></p>

<p>This will kick in as it talks to Ollama in Windows</p>

<p><a href="/content/images/2026/04/opencode-03.png"><img src="/content/images/2026/04/opencode-03.png" alt="/content/images/2026/04/opencode-03.png" /></a></p>

<p>But it too failed</p>

<p><a href="/content/images/2026/04/opencode-04.png"><img src="/content/images/2026/04/opencode-04.png" alt="/content/images/2026/04/opencode-04.png" /></a></p>

<p>Here I feel like the guy with a Roomba who keeps setting it on top of the same piece of trash instead of just picking it up.</p>

<p>Fine, I’ll fix the damn error myself.</p>

<p>When we see:</p>

<blockquote>
  <p>Error: INSTALLATION FAILED: template: fastapi-service/templates/secret.yaml:10:26: executing “fastapi-service/templates/secret.yaml” at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue
helm.go:86: 2026-04-03 14:35:03.126362357 -0500 CDT m=+0.433510017 [debug] template: fastapi-service/templates/secret.yaml:10:26: executing “fastapi-service/templates/secret.yaml” at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue</p>
</blockquote>

<p>It is pretty clear that the secret should be wrapped in an if block and exposed in the values file.</p>

<p>We need to wrap it with an if, ie.g</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{{</span><span class="nv">- if .Values.secret.enabled</span> <span class="pi">}}</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">include "chart.fullname" .</span> <span class="pi">}}</span><span class="s">-secret</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="pi">{{</span><span class="nv">- include "chart.labels" . | nindent 2</span> <span class="pi">}}</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="c1"># Example data key/value pair</span>
  <span class="na">api_key</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">with .Values.secret.apiKeyValue</span> <span class="pi">}}{{</span> <span class="nv">. | b64enc</span> <span class="pi">}}{{</span> <span class="nv">end</span> <span class="pi">}}</span>
<span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
</code></pre></div></div>

<p>Then drop an enable (which is false by default) in the values</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secret:
  enabled: false
  apiKeyValue: ""
</code></pre></div></div>

<p>I fixed the ingress and serviceaccount yamls and then it worked.</p>

<p><strong>Commentary:</strong> Those who say you can just vibe code and don’t need to know things are fooling themselves.</p>

<h1 id="using-the-skills-we-made-with-pi-and-gemma4">Using the Skills we made with Pi and Gemma4</h1>

<p>Again, just as before, you’ll see I have to nudge it along…</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-10.mp4" type="video/mp4" />
</video>

<p>It made a start but really it just gave a start and couldn’t get much farther.</p>

<h2 id="gemini-cli">Gemini CLI</h2>

<p>I plan to just try Gemini CLI now to see if it can finish the job</p>

<p>I’ll give it the same prompt:</p>

<blockquote>
  <p>Create a containerized Perl app using the perl-app-setup skill.  The app should show a list of todos as stored locally in a local
datastore and let the user check off items and add new items.  they can also delete items.</p>
</blockquote>

<p>It very quickly wrapped up, mostly using the Gemini 2.5 flash and 3 flash models</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-11.mp4" type="video/mp4" />
</video>

<h3 id="testing">Testing</h3>

<p>Let’s test the candidate work.</p>

<p>I’ll do a quick docker build</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker build -t todo-app .
[+] Building 64.9s (17/17) FINISHED                                                                                       docker:default
 =&gt; [internal] load build definition from Dockerfile                                                                                0.0s
 =&gt; =&gt; transferring dockerfile: 801B                                                                                                0.0s
 =&gt; [internal] load metadata for docker.io/library/perl:5.38                                                                        1.3s
 =&gt; [internal] load metadata for docker.io/library/perl:5.38-slim                                                                   1.3s
 =&gt; [auth] library/perl:pull token for registry-1.docker.io                                                                         0.0s
 =&gt; [internal] load .dockerignore                                                                                                   0.0s
 =&gt; =&gt; transferring context: 158B                                                                                                   0.0s
 =&gt; [builder 1/6] FROM docker.io/library/perl:5.38@sha256:53060e111843f9b002cdea543dba7bfed54ff78e84b4abdf3b8df00451ea59c6         41.2s
 =&gt; =&gt; resolve docker.io/library/perl:5.38@sha256:53060e111843f9b002cdea543dba7bfed54ff78e84b4abdf3b8df00451ea59c6                  0.0s
 =&gt; =&gt; sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 32B / 32B                                            0.3s
 =&gt; =&gt; sha256:47077a70fcc3f1fb0ef4638f82fd22d1b93ab6a3e98e64f66a157f4ce9ebedba 2.50kB / 2.50kB                                      0.0s
 =&gt; =&gt; sha256:53060e111843f9b002cdea543dba7bfed54ff78e84b4abdf3b8df00451ea59c6 9.01kB / 9.01kB                                      0.0s
 =&gt; =&gt; sha256:c663d63fb5298262de60d5e229e4b408d4c354795fe8019e9c8ccb6456a26613 5.83kB / 5.83kB                                      0.0s
 =&gt; =&gt; sha256:8f6ad858d0a46fa8ee628532c70b8dc82d06179d543b0b09ec19fc03d4c5b373 49.30MB / 49.30MB                                    4.1s
 =&gt; =&gt; sha256:b012eb15dff0bce418c03ec940325aee6aa4300d771c325728855697e620c63a 25.62MB / 25.62MB                                    3.4s
 =&gt; =&gt; sha256:ee3a0e7d77f0c84203cab438fcf345647c8121bbd80506a3c692f8608a14c4f4 67.78MB / 67.78MB                                    7.2s
 =&gt; =&gt; sha256:8688d0f2f567884eb217c6f80efa063bdb13a1951e92e6c5cac1ae5b736f5e1b 236.08MB / 236.08MB                                 12.9s
 =&gt; =&gt; sha256:6623c4016360f6a494137f7a0696634876fd3b5f84b642440e829aca640bd3b2 1.37kB / 1.37kB                                      4.3s
 =&gt; =&gt; extracting sha256:8f6ad858d0a46fa8ee628532c70b8dc82d06179d543b0b09ec19fc03d4c5b373                                           3.6s
 =&gt; =&gt; sha256:dd84d29e92517951df8c584ef70adac3b219a9e94e47073c3477b437fee3d6d3 15.56MB / 15.56MB                                    6.0s
 =&gt; =&gt; sha256:5c58bc0a198b5b32b37cf1fa1d2fc7bd96b5b9145df52faecab5106829908cff 132B / 132B                                          6.3s
 =&gt; =&gt; extracting sha256:b012eb15dff0bce418c03ec940325aee6aa4300d771c325728855697e620c63a                                           1.4s
 =&gt; =&gt; extracting sha256:ee3a0e7d77f0c84203cab438fcf345647c8121bbd80506a3c692f8608a14c4f4                                           5.3s
 =&gt; =&gt; extracting sha256:8688d0f2f567884eb217c6f80efa063bdb13a1951e92e6c5cac1ae5b736f5e1b                                          23.1s
 =&gt; =&gt; extracting sha256:6623c4016360f6a494137f7a0696634876fd3b5f84b642440e829aca640bd3b2                                           0.0s
 =&gt; =&gt; extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1                                           0.0s
 =&gt; =&gt; extracting sha256:dd84d29e92517951df8c584ef70adac3b219a9e94e47073c3477b437fee3d6d3                                           2.4s
 =&gt; =&gt; extracting sha256:5c58bc0a198b5b32b37cf1fa1d2fc7bd96b5b9145df52faecab5106829908cff                                           0.0s
 =&gt; [internal] load build context                                                                                                   0.0s
 =&gt; =&gt; transferring context: 11.19kB                                                                                                0.0s
 =&gt; [runtime 1/4] FROM docker.io/library/perl:5.38-slim@sha256:877a0596aa8b5ef64dd7bde002fac9da4fbcebdc727927ee3cb36d520ad8a5dc     8.3s
 =&gt; =&gt; resolve docker.io/library/perl:5.38-slim@sha256:877a0596aa8b5ef64dd7bde002fac9da4fbcebdc727927ee3cb36d520ad8a5dc             0.0s
 =&gt; =&gt; sha256:c3507f6b13e970e15b0a7f80894ebaf5585dcc16d37c13332bcd4ace74cf9de1 1.37kB / 1.37kB                                      0.4s
 =&gt; =&gt; sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 32B / 32B                                            0.3s
 =&gt; =&gt; sha256:877a0596aa8b5ef64dd7bde002fac9da4fbcebdc727927ee3cb36d520ad8a5dc 9.02kB / 9.02kB                                      0.0s
 =&gt; =&gt; sha256:b82b5ea7daa1a0755078a5297c8ecdf26e7ae43e8e759a372e1df8d0f7570dc9 1.92kB / 1.92kB                                      0.0s
 =&gt; =&gt; sha256:77f07f06f0362b7b34dcaed39280b3ea0f8ec3b0d21492b86b5071e2fa5a7b69 4.75kB / 4.75kB                                      0.0s
 =&gt; =&gt; sha256:ec781dee3f4719c2ca0dd9e73cb1d4ed834ed1a406495eb6e44b6dfaad5d1f8f 29.78MB / 29.78MB                                    1.5s
 =&gt; =&gt; sha256:444f25e108e264b00dae5fb350a4fbac78784586273ef37f290bdd3c8889599f 31.60MB / 31.60MB                                    2.9s
 =&gt; =&gt; sha256:e1debe2bad764c01de94cf2890688af6b6128e923bca730b5814a2696dd457f5 132B / 132B                                          0.7s
 =&gt; =&gt; extracting sha256:ec781dee3f4719c2ca0dd9e73cb1d4ed834ed1a406495eb6e44b6dfaad5d1f8f                                           2.3s
 =&gt; =&gt; extracting sha256:c3507f6b13e970e15b0a7f80894ebaf5585dcc16d37c13332bcd4ace74cf9de1                                           0.0s
 =&gt; =&gt; extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1                                           0.0s
 =&gt; =&gt; extracting sha256:444f25e108e264b00dae5fb350a4fbac78784586273ef37f290bdd3c8889599f                                           3.8s
 =&gt; =&gt; extracting sha256:e1debe2bad764c01de94cf2890688af6b6128e923bca730b5814a2696dd457f5                                           0.0s
 =&gt; [runtime 2/4] WORKDIR /app                                                                                                      1.3s
 =&gt; [builder 2/6] WORKDIR /app                                                                                                      1.8s
 =&gt; [builder 3/6] RUN cpan App::cpanminus &amp;&amp;     apt-get update &amp;&amp; apt-get install -y --no-install-recommends     build-essential  11.8s
 =&gt; [builder 4/6] COPY cpanfile .                                                                                                   0.0s
 =&gt; [builder 5/6] RUN cpanm --installdeps --notest .                                                                                6.3s
 =&gt; [builder 6/6] COPY . .                                                                                                          0.0s
 =&gt; [runtime 3/4] COPY --from=builder /usr/local/lib/perl5 /usr/local/lib/perl5                                                     0.7s
 =&gt; [runtime 4/4] COPY --from=builder /app .                                                                                        0.0s
 =&gt; exporting to image                                                                                                              0.6s
 =&gt; =&gt; exporting layers                                                                                                             0.5s
 =&gt; =&gt; writing image sha256:91c275518148c8e09222cd3c9c1f3db6719bd4105156aabd3cd11035e538f29c                                        0.0s
 =&gt; =&gt; naming to docker.io/library/todo-app                                                                                         0.0s
</code></pre></div></div>

<p>Running it shows this is just a command line app</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker run -it --rm -v $(pwd)/data:/app/data todo-app -l
No todos found.
builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker run -it --rm -v $(pwd)/data:/app/data todo-app
No todos found.
</code></pre></div></div>

<p>I suppose I never demanded it be web-based.  I can just add a task and see it listed</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/newApp$ perl bin/app.pl --add "Complete the coding challenge"
Added todo: Complete the coding challenge (ID: 1)
builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker run -it --rm -v $(pwd)/data:/app/data todo-app
ID  | Status     | Description
----------------------------------------
1   | [ ]        | Complete the coding challenge
</code></pre></div></div>

<h3 id="testing-python-app-skill">Testing Python app skill</h3>

<p>Let’s now test the Kubernetes service Python app skill</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ln -s /home/builder/Workspaces/agentskills/python-app-setup /home/builder/.agents/skills/python-app-setup
</code></pre></div></div>

<p>I’m going to set a higher autocomplete and set steering and followup to all</p>

<p><a href="/content/images/2026/04/pi-10.png"><img src="/content/images/2026/04/pi-10.png" alt="/content/images/2026/04/pi-10.png" /></a></p>

<p>I’m going to pick the skill to sort of prime the pump on it</p>

<p><a href="/content/images/2026/04/pi-11.png"><img src="/content/images/2026/04/pi-11.png" alt="/content/images/2026/04/pi-11.png" /></a></p>

<p>My hope is that with Python, it might move faster than with Perl (which is arguably older and less common).</p>

<p>But time and time again, it failed to actually dot he work it planned</p>

<p><a href="/content/images/2026/04/pi-12.png"><img src="/content/images/2026/04/pi-12.png" alt="/content/images/2026/04/pi-12.png" /></a></p>

<p>However, when I moved to my Linux laptop and ran the same prompt:</p>

<blockquote>
  <p>Create a new project based on the guidelines from the skill my-python-app-skill.  It should be a Todo app that is exposed via
kubernetes and has a web interface.  do not confirm each step, just proceed</p>
</blockquote>

<p>It went way faster and had no issues writing files</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/opencode-05.mp4" type="video/mp4" />
</video>

<p>I decided to test again using Gemma via Continue.dev.</p>

<p>This time I would use the larger video card in the gaming laptop (which is on 192.168.1.220), but with my local windows VS Code</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b (Legion)"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"completionOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
      </span><span class="nl">"apiBase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.220:11434"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:26b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:26b (Legion)"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"completionOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
      </span><span class="nl">"apiBase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.220:11434"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="err">...</span><span class="w">
</span></code></pre></div></div>

<p>I could get it to list files, but not read them when trying to create an Ansible index:</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/opencode-06.mp4" type="video/mp4" />
</video>

<p>using the <code class="language-plaintext highlighter-rouge">@files</code> just sends it to lunch and it never returns.</p>

<p><a href="/content/images/2026/04/gemma4-12.png"><img src="/content/images/2026/04/gemma4-12.png" alt="/content/images/2026/04/gemma4-12.png" /></a></p>

<p>I pivoted to Pi in WSL, but using the more powerful laptop (updating the ~/.pi/agent/models.json)</p>

<p><a href="/content/images/2026/04/pi-13.png"><img src="/content/images/2026/04/pi-13.png" alt="/content/images/2026/04/pi-13.png" /></a></p>

<p>I used <code class="language-plaintext highlighter-rouge">.</code> to try and tell Gemma4 to ‘move it along’</p>

<p><a href="/content/images/2026/04/pi-14.png"><img src="/content/images/2026/04/pi-14.png" alt="/content/images/2026/04/pi-14.png" /></a></p>

<p>But it didn’t update the index</p>

<p><a href="/content/images/2026/04/pi-15.png"><img src="/content/images/2026/04/pi-15.png" alt="/content/images/2026/04/pi-15.png" /></a></p>

<p>I tried several rounds with qwen3.5 as well, being very explicit in my ask - it too failed</p>

<p><a href="/content/images/2026/04/pi-16.png"><img src="/content/images/2026/04/pi-16.png" alt="/content/images/2026/04/pi-16.png" /></a></p>

<p>I even tried <a href="https://github.com/tcsenpai/ollama-code">Ollama code</a></p>

<video muted="" controls="">
    <source src="/content/images/2026/04/ollamacode-01.mp4" type="video/mp4" />
</video>

<p>Is my ask too hard? I needed to sanity check it against Gemini CLI</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/geminicli-01.mp4" type="video/mp4" />
</video>

<p>This just used the flash model and knocked it out.</p>

<p><a href="/content/images/2026/04/pi-17.png"><img src="/content/images/2026/04/pi-17.png" alt="/content/images/2026/04/pi-17.png" /></a></p>

<h2 id="research">Research</h2>

<p>Let’s put a pin in GenAI coding for now.  How about just general research?</p>

<p><a href="/content/images/2026/04/ollamacode-02.png"><img src="/content/images/2026/04/ollamacode-02.png" alt="/content/images/2026/04/ollamacode-02.png" /></a></p>

<p>It made a table, but it’s not readable - likely an Ollama Code issue more than a model issue</p>

<p><a href="/content/images/2026/04/ollamacode-03.png"><img src="/content/images/2026/04/ollamacode-03.png" alt="/content/images/2026/04/ollamacode-03.png" /></a></p>

<p>It immediately forgot it’s last request, so i just rephrased without asking for a table and got a good start for an article</p>

<p><a href="/content/images/2026/04/ollamacode-04.png"><img src="/content/images/2026/04/ollamacode-04.png" alt="/content/images/2026/04/ollamacode-04.png" /></a></p>

<p>I’m not a finance guy, so I wouldn’t really write a piece like that, but I might ask for some examples to punch up a topic I <em>am</em> writing about:</p>

<p>This could be a windows issue, but for the life of me I couldn’t get Ollama code to stop truncating the tables.  I did, however, figure out a workaround - just use a ridiculous large terminal window</p>

<p><a href="/content/images/2026/04/ollamacode-05.png"><img src="/content/images/2026/04/ollamacode-05.png" alt="/content/images/2026/04/ollamacode-05.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>The first part of this article covered a user request to review Anthropics Claude Code versus using the same models in Pi.</p>

<p>The behavior of Claude code was to kick out up to 6 agents to do things, then come back and another 6 agents to evaluate and judge the work.  The output in the end was fine, but it also wasn’t particularly fast.  I found the time I spent building out the helm chart skill with Claude Code was about the same as it was building perl app skill.   The quality <em>might</em> be better with helm but at what price?</p>

<p>I spent about US$0.35 making the Perl skill and about US$2.30 with Claude Code making the helm chart one.  Both used the Anthropic “skill builder” skill.  Assuming the quality is similar, I might use Pi from here on out.  However, as a “free tier” user that pays per token, the money factor might really be my underlying driver.</p>

<p>Next we looked at Gemma4, which Google dropped just last week.  It’s quite thorough and gave good answers.  I tested it in VS Code with Continue.dev, Opencode, Ollama-code and Pi.  I used Windows and Linux.  Overall, I had better success with Pi and Opencode in Linux than anything else.</p>

<p>However, for coding work, Gemma4 is so damn timid.  It just wants to check constantly back with the user.  When you are using lightweight coding agents without much memory, this becomes a real issue as it just keeps forgetting what it was just asking you about and starts back over.  I had really limited success with all my approaches.  I just wanted to tell Gemma4 “just do the damn thing and don’t ask me!” but regardless of how I would phrase things, it would quickly forget and fail to write files.</p>

<p>To sanity check, I would pivot over to a Github free version of Copilot, or using Gemini CLI with Gemini 3 Flash/Flash lite and each time they would complete the work.</p>

<p>I will own that this could be a user error, some kind of “me problem” that needs more investigation.</p>

<p>When I pivoted to using Gemma4 for ideaation it did fantastic.  While I’m not planning to write blog posts on refinancing, using it to come up with ideas for MCP vs Skills is something I might do.   The use case here for such things becomes apparent when you are in the life phase with kids in sports.</p>

<p>Often I’ll be killing time in the backfield of Softball practice or in a swimming pool area built like a Faraday cage and just unable to get signal.  Having an LLM option to help come up with ideas is really useful.</p>

<p>My common case is I’ll be writing a blog, or updating a deck for a presentation and find one area is kind of light on examples.  I’ll ask the LLM something like “provide 5 common ways to migrate a vSphere hosted windows VM to GCP Compute Engine” and then of the 5 returned, 3 might sound plausible to me and get me going again.  It can help provide that spark when my mental matches are wet.</p>]]></content><author><name>Isaac Johnson</name></author><category term="genai" /><category term="claude" /><category term="anthropic" /><category term="gemma" /><category term="opencode" /><category term="pi" /><category term="continue.dev" /><summary type="html"><![CDATA[I had two things I wanted to tackle today: Comparing using Anthropic via Pi CLI versus Claude Code, then taking a look at Gemma4 - the new open model Google released last week. In this article, I want to dig into Gemma4 more thoroughly rather than rush an article just to get on the headline bandwagon.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/ollamapiegb.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/ollamapiegb.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AI Video in 2026</title><link href="https://freshbrewed.science/2026/04/07/aivid.html" rel="alternate" type="text/html" title="AI Video in 2026" /><published>2026-04-07T10:00:01+00:00</published><updated>2026-04-07T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/07/aivid</id><content type="html" xml:base="https://freshbrewed.science/2026/04/07/aivid.html"><![CDATA[<p>A user recently requested by way of the form at the top that I look into <a href="https://kubrix.co/">Kubrix AI</a>.  I wasn’t sure if it the person was involved with the project or genuine interest.  Either way, I warned them I would do an honest take.</p>

<p>While I’m at it, I’ll try a bunch of the prior AI video generation sites I’ve used (Kling, Pollo) as well as my tried and true Gemini and Midjourney.  I’ll also see what I can find that is new (and most important “free”) while I’m at it.</p>

<h1 id="kubrix">Kubrix</h1>

<p>First, the splash screen makes me want to vomit.</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/kubrix-01.mp4" type="video/mp4" />
</video>

<p>I gave it a prompt and then picked a size and duration</p>

<p><a href="/content/images/2026/04/kubrix-02.png"><img src="/content/images/2026/04/kubrix-02.png" alt="/content/images/2026/04/kubrix-02.png" /></a></p>

<p>I used Analog File for a style</p>

<p><a href="/content/images/2026/04/kubrix-03.png"><img src="/content/images/2026/04/kubrix-03.png" alt="/content/images/2026/04/kubrix-03.png" /></a></p>

<p>lighting style</p>

<p><a href="/content/images/2026/04/kubrix-04.png"><img src="/content/images/2026/04/kubrix-04.png" alt="/content/images/2026/04/kubrix-04.png" /></a></p>

<p>A frame</p>

<p><a href="/content/images/2026/04/kubrix-05.png"><img src="/content/images/2026/04/kubrix-05.png" alt="/content/images/2026/04/kubrix-05.png" /></a></p>

<p>Then a model</p>

<p><a href="/content/images/2026/04/kubrix-06.png"><img src="/content/images/2026/04/kubrix-06.png" alt="/content/images/2026/04/kubrix-06.png" /></a></p>

<p>and an optional start image</p>

<p><a href="/content/images/2026/04/kubrix-07.png"><img src="/content/images/2026/04/kubrix-07.png" alt="/content/images/2026/04/kubrix-07.png" /></a></p>

<p>Lastly, a Generate page that says this 4-second video will be “1800 credits”</p>

<p><a href="/content/images/2026/04/kubrix-08.png"><img src="/content/images/2026/04/kubrix-08.png" alt="/content/images/2026/04/kubrix-08.png" /></a></p>

<p>It showed it would use the prompt:</p>

<blockquote>
  <p>A cinematic 4-second opening sequence for a modern ALF reboot. The shot begins with a rapid, smooth dolly-in across a sleek, contemporary suburban living room bathed in the amber glow of a California sunset and accented by cool teal and magenta smart-lighting. ALF, the iconic alien from Melmac, is perched on a minimalist charcoal velvet sofa, looking more realistic than ever with high-fidelity, hand-groomed brown fur textures and expressive, glossy eyes that reflect the room’s ambient light. As the camera zooms in, ALF nonchalantly tosses a high-tech tablet aside, turns toward the lens, and breaks the fourth wall with a signature mischievous smirk and a quick, knowing wink. The atmosphere is vibrant and polished, featuring professional 8K resolution, shallow depth of field that blurs the modern kitchen in the background, and crisp, cinematic color grading. In the final second, a sleek, chrome-textured “ALF” logo with a subtle digital glitch effect emerges in the foreground, pulsing with retro-wave energy.</p>
</blockquote>

<p>I signed in next and see as a new user I get 100 credits and the new price is now only 1000 credits</p>

<p><a href="/content/images/2026/04/kubrix-09.png"><img src="/content/images/2026/04/kubrix-09.png" alt="/content/images/2026/04/kubrix-09.png" /></a></p>

<p>This leads me to “how to get credits”.</p>

<p>I can either subscribe to a monthly fee, the lowest of which is US$30</p>

<p><a href="/content/images/2026/04/kubrix-10.png"><img src="/content/images/2026/04/kubrix-10.png" alt="/content/images/2026/04/kubrix-10.png" /></a></p>

<p>or a one-time top up for $40</p>

<p><a href="/content/images/2026/04/kubrix-11.png"><img src="/content/images/2026/04/kubrix-11.png" alt="/content/images/2026/04/kubrix-11.png" /></a></p>

<p>I was willing to deal with at most US$10 to try a thing so unfortunately we will have to pass.</p>

<h1 id="midjourney">Midjourney</h1>

<p>I was curious what Midjourney would do with that prompt.  Even though I mostly use Gemini now when I need a bit of AI graphics, I still pay for MJ as it was the first that really offered solid AI Generated art.</p>

<p>As you can see, it’s generally best to get some images then run a video from the best of them (if there is a best of them)</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/midjourney-01.mp4" type="video/mp4" />
</video>

<p>I tried the same prompt, but this time fed it an ALF still to start</p>

<p>The text is a mess, but I was surprised how it accurately figured out how Shumway talked just from a still</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/midjourney-02.mp4" type="video/mp4" />
</video>

<p>and a regen</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/midjourney-03.mp4" type="video/mp4" />
</video>

<h1 id="gemini-veo2-video">Gemini (VEO2) video</h1>

<p>Let’s put the same thing in Gemini to see what it comes up with</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemini-01.mp4" type="video/mp4" />
</video>

<p>Here is the video (with some strange sound effects)</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-02.mp4" type="video/mp4" />
</video>

<p>I gave it an image to refine the video</p>

<p><a href="/content/images/2026/04/gemini-03.png"><img src="/content/images/2026/04/gemini-03.png" alt="/content/images/2026/04/gemini-03.png" /></a></p>

<p>which did better visually</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-04.mp4" type="video/mp4" />
</video>

<p>One last refinement. I asked “Can you give him a deeper New York snarky comedic voice”?</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-05.mp4" type="video/mp4" />
</video>

<p>I tried again the next day…</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-06.mp4" type="video/mp4" />
</video>

<p>When I asked it to tweak the voice for a NJ accent, it re-imagined the look again</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-07.mp4" type="video/mp4" />
</video>

<p>This highlights one small limit.  Using Google AI Pro (which I got a year of with my last Google Pixel Fold phone), I get 3 videos a day.  So to do more, I either pay out of pocket in AI Studio in GCP or I just wait till tomorrow.</p>

<h1 id="kling">Kling</h1>

<p>Next I gave Kling a try</p>

<p><a href="/content/images/2026/04/kling-01.png"><img src="/content/images/2026/04/kling-01.png" alt="/content/images/2026/04/kling-01.png" /></a></p>

<p>But even though I have sufficient credits, it would seem they paused the free tier</p>

<p><a href="/content/images/2026/04/kling-02.png"><img src="/content/images/2026/04/kling-02.png" alt="/content/images/2026/04/kling-02.png" /></a></p>

<p>I came back a couple days in a row just to check, but it was still blocking free tier.</p>

<h1 id="self-comfyui">Self (ComfyUI)</h1>

<p>As we’ve shown, using something like ComfyUI on a computer with a GPU, one can just create these kind of things locally.</p>

<p>I fired up ComfyUI.</p>

<p>I’m going to use a Wan Vace 14b template I have bookmarked.</p>

<p>It might looking daunting, but really I just need to give a seed image and a prompt (see the arrows)</p>

<p><a href="/content/images/2026/04/comfyui-01.png"><img src="/content/images/2026/04/comfyui-01.png" alt="/content/images/2026/04/comfyui-01.png" /></a></p>

<p>I’ve translated those Chinese statements in the negative block before and they are pretty canned “no watermark, no junk, etc…”.</p>

<video controls="">
    <source src="/content/images/2026/04/ComfyUI_00006_.mp4" type="video/mp4" />
</video>

<p>I then tried with LTX2 (needed a 40Gb model downloaded)</p>

<p><a href="/content/images/2026/04/comfyui-02.png"><img src="/content/images/2026/04/comfyui-02.png" alt="/content/images/2026/04/comfyui-02.png" /></a></p>

<p>a little weird but could be a start</p>

<video controls="">
    <source src="/content/images/2026/04/LTX-2_00016_.mp4" type="video/mp4" />
</video>

<p>I tried LTX 2.3 which can take a video input with the same prompt and ALF still</p>

<p><a href="/content/images/2026/04/comfyui-03.png"><img src="/content/images/2026/04/comfyui-03.png" alt="/content/images/2026/04/comfyui-03.png" /></a></p>

<p>Though it gave me the oddest output</p>

<video controls="">
    <source src="/content/images/2026/04/LTX_2.3_t2v_00001_.mp4" type="video/mp4" />
</video>

<p>A second pass with just asking for a “The TV intro for an ALF sitcom reboot.” (but also with the still)</p>

<video controls="">
    <source src="/content/images/2026/04/LTX_2.3_t2v_00002_.mp4" type="video/mp4" />
</video>

<p>One last go, I set the prompt to “The TV intro for an ALF sitcom reboot.  Should include this character”</p>

<video controls="">
    <source src="/content/images/2026/04/LTX_2.3_t2v_00003_.mp4" type="video/mp4" />
</video>

<h1 id="seevideoai">Seevideo.ai</h1>

<p>I found an <a href="https://seevideo.ai/">AI Video site, seevideo.ai</a> that would give me just enough first-time credits (100) to try Seedance 1.5.  It isn’t the latest 2.0, but it was worth a try</p>

<p><a href="/content/images/2026/04/seevideoai-01.png"><img src="/content/images/2026/04/seevideoai-01.png" alt="/content/images/2026/04/seevideoai-01.png" /></a></p>

<video controls="">
    <source src="/content/images/2026/04/seevideoai-01.mp4" type="video/mp4" />
</video>

<h1 id="dreamina-capcut">Dreamina capcut</h1>

<p>Another one that gave me just enough for Seedance 1.5 Pro was <a href="https://dreamina.capcut.com/">Dreamina</a></p>

<p><a href="/content/images/2026/04/dreamina-01.png"><img src="/content/images/2026/04/dreamina-01.png" alt="/content/images/2026/04/dreamina-01.png" /></a></p>

<video controls="">
    <source src="/content/images/2026/04/dreamina-01.mp4" type="video/mp4" />
</video>

<h1 id="pollo-ai">Pollo AI</h1>

<p><a href="https://pollo.ai/">Pollo.ai</a> has it’s own model as well as others.  I’ve used them in the past for some nice intro videos.</p>

<p><a href="/content/images/2026/04/pollo-01.png"><img src="/content/images/2026/04/pollo-01.png" alt="/content/images/2026/04/pollo-01.png" /></a></p>

<video controls="">
    <source src="/content/images/2026/04/pollo-01.mp4" type="video/mp4" />
</video>

<p>It was interesting how it showed the still but then just did it’s own thing</p>

<h1 id="seedance2ai">Seedance2ai</h1>

<p>One that wouldn’t let me use Seedance2 (for pro customers), but did let me try the now defunct Sora2 was <a href="https://www.seedance2ai.io/">Seedance2ai</a></p>

<p><a href="/content/images/2026/04/seedance2ai-01.png"><img src="/content/images/2026/04/seedance2ai-01.png" alt="/content/images/2026/04/seedance2ai-01.png" /></a></p>

<p>But I have a feeling it will never complete</p>

<p><a href="/content/images/2026/04/seedance2ai-02.png"><img src="/content/images/2026/04/seedance2ai-02.png" alt="/content/images/2026/04/seedance2ai-02.png" /></a></p>

<p>In the end my generated workspace disappeared (but didn’t charge the credits)</p>

<h1 id="summary">Summary</h1>

<p>We tried many many AI video generators today.  It is not that surprising that we can find an endless assortment of new AI startups that will give just enough credits to try a video or two so I question those video generation sites that give new users nothing.  There is no real motivation.</p>

<p>This post started with a user request to review <a href="https://kubrix.co/">Kubrix AI</a>.  Since they offer me nothing and have this awful background video, I can say confidently I won’t be returning.  Of the new ones, the closest I got to paying was <a href="https://www.seedance2ai.io/">Seedance2ai.io</a> as if I dropped the coin for an annual plan, I could have used Seedance2.  That said, <a href="https://soro2.ai/">soro2.ai</a> would let me if I dropped a one-time $50 top up.</p>

<p>I could have used PowerDirector as I have 100 credits in there as an annual subscriber. but they didn’t offer any unique models I wanted to try</p>

<p><a href="/content/images/2026/04/powerdirector-01.png"><img src="/content/images/2026/04/powerdirector-01.png" alt="/content/images/2026/04/powerdirector-01.png" /></a></p>

<p>If I was tasked on making a TV intro with just the tools at hand, I would likely splice some MJ clips together and use <a href="https://suno.com/">Suno</a> to make a Jingle…</p>

<p><a href="/content/images/2026/04/suno-01.png"><img src="/content/images/2026/04/suno-01.png" alt="/content/images/2026/04/suno-01.png" /></a></p>

<p>then create with a style</p>

<p><a href="/content/images/2026/04/suno-02.png"><img src="/content/images/2026/04/suno-02.png" alt="/content/images/2026/04/suno-02.png" /></a></p>

<p>Something like</p>

<video controls="">
    <source src="/content/images/2026/04/ALFJingle.mp4" type="video/mp4" />
</video>]]></content><author><name>Isaac Johnson</name></author><category term="GenAI" /><category term="kling" /><category term="pollo" /><category term="seedance" /><category term="gemini" /><category term="ComfyUI" /><category term="midjourney" /><summary type="html"><![CDATA[A user recently requested by way of the form at the top that I look into Kubrix AI. I wasn’t sure if it the person was involved with the project or genuine interest. Either way, I warned them I would do an honest take.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/alfstill.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/alfstill.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>