End-to-End Apps with Claude Code: Part 2

Published: Mar 15, 2025 by Isaac Johnson

After I wrapped the last post I just wanted to keep going.

One of my friends mentioned he forgot his password. I realized without a password reset feature, this app is going to have issues.

Adding Password Resets

I fired the same prompt at both GPT 4.o via Azure and Sonnet 3.5 via Copilot Free tier

I want to add a password reset feature. This means I need to allow users to add an optional email address at register and landing page. I want code to use email (Sendgrid) to send a password reset link that would then have a password reset page they could change their password.

/content/images/2025/03/meetapp-01.png

There were a lot of changes, but some hardcoded values.

I prompted to get them added to the Dockerfile

/content/images/2025/03/meetapp-02.png

and then asked to handle the “From” address. But note, it wanted to make the expiry time stamp funky so i noted that in a comment block, but left the last change in place

/content/images/2025/03/meetapp-03.png

Lastly, I really did not want to take the risk I would leave my SG API key checked into a YAML, so I set it up to pull from a Secret I would need to create manually and noted the steps in the README.md

/content/images/2025/03/meetapp-04.png

Release my last

The last version is quite good. I want to denote a release of it as it stands

/content/images/2025/03/meetapp-05.png

Test instance.

I do not want to roll live as this one will need a DB migration.

I want to test that first. I think I’ll create a “beta” site I can use:

$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n meet-beta
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "49963893-185e-41e0-923b-4b4b3895198f",
  "fqdn": "meet-beta.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/meet-beta",
  "name": "meet-beta",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

I’m not sure if you’ve ever gotten into a groove like I did this morning (credit to a French press of DeathWish coffee and some good PrimeThantos) but I just kept writing and writing and realized I better stop and cut a release.

/content/images/2025/03/meetapp-06.png

I’ve added in this one commit (yes, thump me as I would thump a junior that did the same):

  • New Helm charts
  • Database migrations
  • Updated user DB for emails and reset tokens
  • a reset page and Sendgrid backend for emails
  • Updated docker files, manifest, helm to handle emails and Sendgrid secrets
  • Readme files for DB Migrations, helm install, secrets
  • Readme update for how to backup prod DB (and a test of my ProdDB)

When I did go to AzDO to invoke a release, it dawned on me it might be smart to retain the last release:

/content/images/2025/03/meetapp-08.png

I can always stop retaining it as well:

/content/images/2025/03/meetapp-07.png

I now have a new build “21130” that is ready to test.

Testing Migrations

Let’s install the last release before my change using the new Ingress URL and settings.

I like to use “–dr-run”

$ helm install --dry-run --set secrets.sendgrid.name=beta-sendgrid-secret --set secrets.sendgrid.value="SG.FAKE" --set ingress.host=meet-beta.tpk.pw --set ingress.tls.secretName=meet-beta-tls --set image.tag=21129 --set imagePullSecrets[0].name=myharborreg --set migration.enabled=false meetbeta ./helm/devotion/
NAME: meetbeta
LAST DEPLOYED: Sat Mar  1 10:52:30 2025
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: devotion/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: beta-sendgrid-secret
  labels:
    helm.sh/chart: devotion-0.1.0
    app.kubernetes.io/name: devotion
    app.kubernetes.io/instance: meetbeta
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/managed-by: Helm
type: Opaque
data:
  api-key: U0cuRkFLRQ==
---
# Source: devotion/templates/pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: meetbeta-devotion-pvc
  labels:
    helm.sh/chart: devotion-0.1.0
    app.kubernetes.io/name: devotion
    app.kubernetes.io/instance: meetbeta
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/managed-by: Helm
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
# Source: devotion/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: meetbeta-devotion-service
  labels:
    helm.sh/chart: devotion-0.1.0
    app.kubernetes.io/name: devotion
    app.kubernetes.io/instance: meetbeta
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/managed-by: Helm
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 5000
      protocol: TCP
  selector:
    app: devotion-app
    app.kubernetes.io/name: devotion
    app.kubernetes.io/instance: meetbeta
---
# Source: devotion/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: meetbeta-devotion-app
  labels:
    app: devotion-app
    helm.sh/chart: devotion-0.1.0
    app.kubernetes.io/name: devotion
    app.kubernetes.io/instance: meetbeta
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: devotion-app
      app.kubernetes.io/name: devotion
      app.kubernetes.io/instance: meetbeta
  template:
    metadata:
      labels:
        app: devotion-app
        app.kubernetes.io/name: devotion
        app.kubernetes.io/instance: meetbeta
    spec:
      imagePullSecrets:
        - name: myharborreg
      containers:
      - name: devotion-app
        image: "harbor.freshbrewed.science/freshbrewedprivate/meet:21129"
        imagePullPolicy: IfNotPresent
        env:
        - name: DATABASE_PATH
          value: /mnt/db
        - name: SENDGRID_API_KEY
          valueFrom:
            secretKeyRef:
              name: beta-sendgrid-secret
              key: api-key
        - name: SG_FROM_ADDRESS
          value: isaac@freshbrewed.science
        ports:
        - containerPort: 5000
        volumeMounts:
        - name: db-storage
          mountPath: /mnt/db
      volumes:
      - name: db-storage
        persistentVolumeClaim:
          claimName: meetbeta-devotion-pvc
---
# Source: devotion/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: meetbeta-devotion-ingress
  labels:
    helm.sh/chart: devotion-0.1.0
    app.kubernetes.io/name: devotion
    app.kubernetes.io/instance: meetbeta
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/managed-by: Helm
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
  ingressClassName: nginx
  rules:
  - host: meet-beta.tpk.pw
    http:
      paths:
      - path: /
        pathType: ImplementationSpecific
        backend:
          service:
            name: meetbeta-devotion-service
            port:
              number: 80
  tls:
  - hosts:
    - meet-beta.tpk.pw
    secretName: meet-beta-tls

Let’s fire it off

$ helm install --set secrets.sendgrid.name=beta-sendgrid-secret --set secrets.sendgrid.value="SG.FAKE" --set ingress.host=meet-beta.tpk.pw --set ingress.tls.secretName=meet-beta-tls --set image.tag=21129 --se
t imagePullSecrets[0].name=myharborreg --set migration.enabled=false meetbeta ./helm/devotion/
NAME: meetbeta
LAST DEPLOYED: Sat Mar  1 10:54:36 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

When I see the cert is satisified

$ kubectl get cert meet-beta-tls
NAME            READY   SECRET          AGE
meet-beta-tls   True    meet-beta-tls   88s

I can then test:

/content/images/2025/03/meetapp-09.png

I tested creating users, resetting passwords, admin pages, teams, notes and schedules.

Now I’ll try launching an upgrade with helm.

$ helm list
NAME                            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                           APP VERSION
easyappt                        default         6               2024-11-04 19:51:47.518777777 -0600 CST deployed        EasyAppointments-0.1.1
harbor-registry                 default         1               2024-03-01 21:16:35.747626288 -0600 CST deployed        harbor-1.14.0                   2.10.0
kimai                           default         3               2025-01-22 06:45:59.199836843 -0600 CST deployed        kimai2-4.3.1                    apache-2.27.0
matrix-synapse                  default         1               2024-03-02 08:37:10.310138818 -0600 CST deployed        matrix-synapse-3.8.2            1.101.0
meetbeta                        default         1               2025-03-01 10:54:36.677021634 -0600 CST deployed        devotion-0.1.0                  1.0.0

We can fetch the values we used before:

builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ helm get values meetbeta -o yaml > meetbeta.values.yaml
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ vi meetbeta.values.yaml
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ cat meetbeta.values.yaml
image:
  tag: 21129
imagePullSecrets:
- name: myharborreg
ingress:
  host: meet-beta.tpk.pw
  tls:
    secretName: meet-beta-tls
migration:
  enabled: false
secrets:
  sendgrid:
    name: beta-sendgrid-secret
    value: SG.FAKE

I’ll now set the real SG API key there, and update the image and set migrations.

/content/images/2025/03/meetapp-10.png

Helm seemed to go out to lunch

$ helm upgrade meetbeta -f meetbeta.values.yaml ./helm/devotion/

I found the migration pods were cycling

meetbeta-devotion-app-779b9fdd67-hd2qf               1/1     Running     0                11m
meetbeta-db-migrate-6p8mz                            0/1     Error       0                47s
meetbeta-db-migrate-44tts                            0/1     Error       0                37s
meetbeta-db-migrate-rlk5r                            0/1     Error       0                23s

the logs seem to show it vomits on the user table (which is why i wanted a migrate in the first place)

$ kubectl logs meetbeta-db-migrate-9nr2v
When W
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: user.email
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.name AS user_name, user.password AS user_password, user.email AS user_email, user.is_admin AS user_is_admin, user.reset_token AS user_reset_token, user.reset_token_expiry AS user_reset_token_expiry
FROM user
WHERE user.username = ?
 LIMIT ? OFFSET ?]
[parameters: ('admin', 1, 0)]
(Background on this error at: https://sqlalche.me/e/14/e3q8)

I tried over and over with Copilot Free using the 3.5 Sonnet model as well as GPT 4.

Let’s give Claude Code a shot at this:

My first shot gave an error:

builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ head -n 4 ./meetbeta.values.yaml
image:
        tag: 21132
imagePullSecrets:
- name: myharborreg
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ helm upgrade meetbeta -f meetbeta.values.yaml ./helm/devotion/
Error: UPGRADE FAILED: YAML parse error on devotion/templates/migrate-job.yaml: error converting YAML to JSON: yaml: line 39: could not find expected ':'

just caused by missing indentation in the migration job

/content/images/2025/03/meetapp-12.png

I gave it to Claude Code again

/content/images/2025/03/meetapp-13.png

As the change was entirely in the migration YAML:

/content/images/2025/03/meetapp-14.png

i don’t need to build a container, just try helm again

Password Resets with Emails

Another issue I realized was emails. If I have a reset, people should be able to change or set their email addresses and the admin page should be able to do the same.

I asked Claude Code for help here

/content/images/2025/03/meetapp-15.png

There were a couple minor bugs like the Forgot Password page layout was a bit wonk

/content/images/2025/03/meetapp-16.png

The emails also were not sending. However, once I put in some debug code to figure out why:

/content/images/2025/03/meetapp-19.png

they magically just started to work

/content/images/2025/03/meetapp-18.png

But with some prompting and fixing of missing style blocks, I got even the password reset page to look okay

/content/images/2025/03/meetapp-17.png

Later, I closed Claude Code and noticed for the first time it actually tells me how much the session costed me.

/content/images/2025/03/meetapp-42.png

Organizing Work

That evening my mind kept coming up with more and more features. I decided to start to use Forgejo boards to gather the work.

I created issues for the ideas

/content/images/2025/03/meetapp-20.png

I created a new project and set it to Kanban style

/content/images/2025/03/meetapp-22.png

I then went to the issues and set them to the new project

/content/images/2025/03/meetapp-21.png

/content/images/2025/03/meetapp-24.png

I can now see them all in the first Kanban stage

/content/images/2025/03/meetapp-23.png

I can now move them around for ordering and set from Uncategorized to “To Do”

A Development flow

I can now start treating this like a real dev project.

I’ll take an issue and start the timer

/content/images/2025/03/meetapp-26.png

I’ll use the Issue ID in my branch name

builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git checkout -b 3-Team-Password-Reset
Switched to a new branch '3-Team-Password-Reset'

and I’ll move the issue into “In Progress”

/content/images/2025/03/meetapp-27.png

I’ll try Gemini Code Assist for this first item. Though, unlike the Continue.dev plugin and Copilot, we cannot just pass the whole codebase. Instead, we have to feed just the files in question

/content/images/2025/03/meetapp-28.png

Unlike the other tools, it doesn’t seem to be able to edit my code direct. The diff is generally wrong as well

/content/images/2025/03/meetapp-29.png

Though I can find a spot to add this new app.route in the app.py

/content/images/2025/03/meetapp-30.png

For new files, I needed to add a new file manually, then insert into new file. I actually am not bothered by this as sometimes Continue and Copilot come back with garbled output that has the links missing and I just copy it over this way when it happens.

I not only caught i used the wrong templates folder and corrected that, but there were cases I needed to intrepet the results and insert the block in the correct area

/content/images/2025/03/meetapp-32.png

The other nuance is that Gemini didn’t fill in the full change_team_password page so I needed to add the top blocks with style and bottom tags to close the div, body and HTML.

/content/images/2025/03/meetapp-33.png

I’ll go ahead and push a new branch with these changes

builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git status
On branch 3-Team-Password-Reset
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   app.py
        modified:   templates/admin.html
        modified:   templates/landing.html

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        templates/change_team_password.html

no changes added to commit (use "git add" and/or "git commit -a")
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git add templates/
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git add app.py 
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git branch --show-current
3-Team-Password-Reset
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git commit -m "#3 - team password change and show"
[3-Team-Password-Reset fe969bf] #3 - team password change and show
 4 files changed, 174 insertions(+)
 create mode 100644 templates/change_team_password.html
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git push
fatal: The current branch 3-Team-Password-Reset has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin 3-Team-Password-Reset

builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git push --set-upstream origin 3-Team-Password-Reset
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 16 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (7/7), 2.18 KiB | 2.18 MiB/s, done.
Total 7 (delta 5), reused 0 (delta 0)
remote: 
remote: Create a new pull request for '3-Team-Password-Reset':
remote:   https://forgejo.freshbrewed.science/builderadmin/flaskAppBase/compare/main...3-Team-Password-Reset
remote: 
remote: . Processing 1 references
remote: Processed 1 references in total
To https://forgejo.freshbrewed.science/builderadmin/flaskAppBase.git
 * [new branch]      3-Team-Password-Reset -> 3-Team-Password-Reset
Branch '3-Team-Password-Reset' set up to track remote branch '3-Team-Password-Reset' from 'origin'.

I’ll indicate the branch on the issue

/content/images/2025/03/meetapp-34.png

Due to the disconnected nature of my Azure Pipelines, I did have to paste in the branch name as the drop down does not work

/content/images/2025/03/meetapp-35.png

Now to just test the latest build in beta

/content/images/2025/03/meetapp-36.png

I can use this one-liner to upgrade the beta site

$ helm get values meetbeta -o yaml | sed 's/tag: .*/tag: 21136/g' > meetbeta.values.yaml && helm upgrade -f ./meetbeta.values.y
aml meetbeta ./helm/devotion/
Release "meetbeta" has been upgraded. Happy Helming!
NAME: meetbeta
LAST DEPLOYED: Sun Mar  2 12:19:23 2025
NAMESPACE: default
STATUS: deployed
REVISION: 10
TEST SUITE: None

My pod was crashing - i clearly made a mistake:

meetbeta-devotion-app-86cf74447f-xms9p               0/1     Error       3 (38s ago)      55s
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ kubectl logs meetbeta-devotion-app-86cf74447f-xms9p
Traceback (most recent call last):
  File "/usr/local/bin/flask", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.9/site-packages/flask/cli.py", line 1047, in main
    cli.main()
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1082, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1697, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1443, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/click/decorators.py", line 92, in new_func
    return ctx.invoke(f, obj, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/flask/cli.py", line 911, in run_command
    raise e from None
  File "/usr/local/lib/python3.9/site-packages/flask/cli.py", line 897, in run_command
    app = info.load_app()
  File "/usr/local/lib/python3.9/site-packages/flask/cli.py", line 312, in load_app
    app = locate_app(import_name, None, raise_if_not_found=False)
  File "/usr/local/lib/python3.9/site-packages/flask/cli.py", line 218, in locate_app
    __import__(module_name)
  File "/app/app.py", line 739, in <module>
    def change_team_password():
  File "/usr/local/lib/python3.9/site-packages/flask/scaffold.py", line 449, in decorator
    self.add_url_rule(rule, endpoint, f, **options)
  File "/usr/local/lib/python3.9/site-packages/flask/scaffold.py", line 50, in wrapper_func
    return f(self, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1358, in add_url_rule
    raise AssertionError(
AssertionError: View function mapping is overwriting an existing endpoint function: change_team_password

Indeed I doubled up on my routes. I copied the Gemini suggested block over the old one and pushed

builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git add app.py 
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git commit -m "#3: combine change_team_passwords"
[3-Team-Password-Reset d135775] #3: combine change_team_passwords
 1 file changed, 23 insertions(+), 43 deletions(-)
builder@DESKTOP-QADGF36:~/Workspaces/flaskAppBase$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 331 bytes | 331.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: 
remote: Create a new pull request for '3-Team-Password-Reset':
remote:   https://forgejo.freshbrewed.science/builderadmin/flaskAppBase/compare/main...3-Team-Password-Reset
remote: 
remote: . Processing 1 references
remote: Processed 1 references in total
To https://forgejo.freshbrewed.science/builderadmin/flaskAppBase.git
   fe969bf..d135775  3-Team-Password-Reset -> 3-Team-Password-Reset

Fired a fresh build

/content/images/2025/03/meetapp-37.png

Then tested. I had to do a few tweaks to the Landing page. When it’s just one file, I sometimes do them in the Forgejo UI instead of VS Code

/content/images/2025/03/meetapp-38.png

Now that it works

/content/images/2025/03/meetapp-39.png

I can create a Merge Commit back to the main devotionApp branch

/content/images/2025/03/meetapp-40.png

In my release board, I can see it has been closed

/content/images/2025/03/meetapp-41.png

Claude Code

In my next fix, the DB Migrations provided by Claude 3.5 via Copilot Free just weren’t working.

I went to Claude Code to fix

/content/images/2025/03/meetapp-43.png

I won’t show every iteration, but in the end it took 5 rounds through to get them all sorted out to properly migrate the Teams table.

The end cost was US$0.14

/content/images/2025/03/meetapp-44.png

Perhaps this is the smart move. Using Claude 3.5 and GPT via Copilot free then only popping over to Claude Code with 3.7 when I’m stuck.

As I got stuck in more DB Migrate errors, I had Claude Code help once again. In two rounds it was another $0.09

/content/images/2025/03/meetapp-45.png

But when done, I had implemented the new Public URL feature (or Claude did to really give credit where it’s due).

/content/images/2025/03/meetapp-46.png

While I had not been explicit, I was happy to find in testing that “unsharing” worked just as well

/content/images/2025/03/meetapp-47.png

I should point out that creating a MR now, noting “Fixes” in the description

/content/images/2025/03/meetapp-48.png

Will associate the MR with the Issue

/content/images/2025/03/meetapp-49.png

And when I merge and delete the branch

/content/images/2025/03/meetapp-50.png

This closes the issue

/content/images/2025/03/meetapp-51.png

I decided to make a Milestone to group these issues

/content/images/2025/03/meetapp-52.png

Now seen under “Done”

/content/images/2025/03/meetapp-53.png

I will also add a 1.2 tag to the Harbor CR

/content/images/2025/03/meetapp-54.png

Now I can try upgrading my production instance.

$ helm list
NAME                            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                           APP VERSION
meetbeta                        default         21              2025-03-02 16:54:49.558728646 -0600 CST deployed        devotion-0.2.0                  1.0.2
meetmain                        default         3               2025-03-01 19:50:45.939736166 -0600 CST deployed        devotion-0.1.2                  1.0.1


$ helm get values meetmain -o yaml | sed 's/tag: .*/tag: 1.2/g' > meetmain.values.yaml && helm upgrade -f ./meetmain.values.yam
l meetmain ./helm/devotion/
Release "meetmain" has been upgraded. Happy Helming!
NAME: meetmain
LAST DEPLOYED: Sun Mar  2 19:54:51 2025
NAMESPACE: default
STATUS: deployed
REVISION: 4
TEST SUITE: None


$ helm list
NAME                            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                           APP VERSION
meetbeta                        default         21              2025-03-02 16:54:49.558728646 -0600 CST deployed        devotion-0.2.0                  1.0.2
meetmain                        default         4               2025-03-02 19:54:51.168990783 -0600 CST deployed        devotion-0.2.0                  1.0.2

I can now see my changes are live on the main Meet app (and the data has persisted)

/content/images/2025/03/meetapp-55.png

Home page

I wanted something a bit fresher than a bland “login” for first time visitors

/content/images/2025/03/meetapp-56.png

This kind of irks me. It tried something, failed, and cost me $0.36

/content/images/2025/03/meetapp-57.png

and again

● Read(file_path: "app.py")…
  ⎿  from flask import Flask, session, request, jsonify, redirect, url_for, render_template, send_from_directory, Response
     from flask_sqlalchemy import SQLAlchemy
     import os
     ... (+865 lines)

● Read(file_path: "images/coffee_logo_red.jpg")…
  ⎿  Read image
  ⎿  API Error: 400
{"type":"error","error":{"type":"invalid_request_error","message":"messages.8.content.0.tool_result.content.0.image.source.base64.media_type:
Input should be 'image/jpeg', 'image/png', 'image/gif' or 'image/webp'"}}

> try again, you got an error
  ⎿  API Error: 400
{"type":"error","error":{"type":"invalid_request_error","message":"messages.8.content.0.tool_result.content.0.image.source.base64.media_type:
Input should be 'image/jpeg', 'image/png', 'image/gif' or 'image/webp'"}}

╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ >                                                                                                                                             │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
  Press Ctrl-C again to exit                                                                                                     \⏎ for newline



Total cost: $0.0675
Total duration (API): 12.5s
Total duration (wall): 48.9s

I found myself going back and forth between free tools and Claude.

One of the updates I made to make life a bit easier was settign up a proper WIF Azure RM account and letting AzDO update the Meet Beta as part of the build:

/content/images/2025/03/meetapp-58.png

Pipeline Updates

One thing I found myself doing over and over was building a candidate branch and pushing to https://meet-beta.tpk.pw.

I thought, “why can’t I just do it in a pipeline?”

I’ll save you the story on the iterations it took to get it working, but the resulting pipeline looks like:

/content/images/2025/03/meetapp-59.png

What it really took was fetching my exposed kubeconfig from AKV

az keyvault secret show --vault-name MYAKV --name MYEXTK8SCONFIG | jq -r .value > ~/.kube/config

/content/images/2025/03/meetapp-60.png

Then pulling and updating the current meetbeta deployment withe new build ID i just pushed

helm list -A

echo "== update ===="

helm get values meetbeta -n default  | tee $(Build.StagingDirectory)/meetbeta.values.yaml
sed -i 's/tag: .*/tag: $(Build.BuildID)/g' $(Build.StagingDirectory)/meetbeta.values.yaml
cat $(Build.StagingDirectory)/meetbeta.values.yaml

echo "== upgrade===="

helm upgrade -f $(Build.StagingDirectory)/meetbeta.values.yaml meetbeta ./helm/devotion/

/content/images/2025/03/meetapp-61.png

I then build out a Release Pipeline that if left with defaults, skips the build and update steps

/content/images/2025/03/meetapp-62.png

by way of a custom condition checking on the default

and(succeeded(),ne(variables['ReleaseID'], '1.x'))

/content/images/2025/03/meetapp-63.png

This means I can do a flow such as test latest mainline on meet-beta

And after seeing it’s working, promoted to a full release

If I wanted to move to Azure Repos or Github where AzDO supports YAML pipelines, I could just export the Classic UI as YAML

/content/images/2025/03/meetapp-66.png

I actually think it’s a bit of a miss that Azure DevOps doesn’t support YAML outside of Github, Azure Repos and Bitbucket.

Summary

I started by attacking some wish list features, like a password reset which meant adding and testing email services.

/content/images/2025/03/meetapp-73.png

Having a token now and email password added to the user table meant solving Database changes.

I spent the most time (and money by way of Anthropic Claude Code) creating working DB migrations

/content/images/2025/03/meetapp-69.png

We created Issues and a Release board to track our work

/content/images/2025/03/meetapp-70.png

Milestones for gathering work into releases

/content/images/2025/03/meetapp-71.png

And of course, Releases themselves

/content/images/2025/03/meetapp-72.png

We made a CI and Deployment Pipeline that works to test and release the App in Azure DevOps

/content/images/2025/03/meetapp-67.png

Publishing with Release tags to an internal Harbor CR registry

/content/images/2025/03/meetapp-68.png

In doing all this I used a mix of Claude Code with Anthropic, Google Gemini Code Assist, Github Copilot (Free) with Claude 3.5 Sonnet and ChatGPT by way of Azure AI.

My big takeaways were:

  1. Use the free tools for small stuff if you aren’t able to just quickly make the change yourself
  2. Using very specific prompts on the cheaper AI tools or older models really helps direct it
  3. Only after trying a few times with free/low-cost AI tools, move to paid newer ones like Claude Code
  4. CICD helps us fail fast. I inherently know this as a DevOps/SRE engineer, but sometimes I need that reminder
    1. Corollary: save often, create commits. if something goes wonk, reset and do over.
AI Claude Python ClaudeCode

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes