BlueSky Posting App Updates and Helm Charts

Published: Dec 19, 2024 by Isaac Johnson

I recently noticed long social slugs were still breaking my BlueSky app poster when updating this blog.

In this post we debug then update the Python code to check for size and trim. We also build out helm charts, Github workflows that leverage my on-prem Harbor CR (in a public shared repo). The result is an expose RESTful endpoint anyone can use to post to BlueSky

Checking the errors

I noted that a post still was blocked

/content/images/2024/12/bluesky-08.png

<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

I was able to test and replicate locally

$ cat bsky.payload2.json
{"USERNAME": "isaacj.bsky.social", "PASSWORD": "xxxxxxxxxxxxxx", "TEXT": "When one builds a poor man's data center in the basement with discarded and obsolete old tech, there are bound to be failures.  Today I wanted to share a few find-and-fix moments maintaining a functioning home lab.  From MinIO and NAS Updates to disk remediation.  We even have Phantom pages and laptop batteries that are going full marshmallow man (stayin puft).", "LINK": "https://go.tpk.pw/qzpq5"}

$ curl -X POST https://bskyposter.steeped.space/post -H 'Content-Type: application/json' -d @bsky.payload2.json
<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

I went to the cluster to check the logs of the container

$ kubectl logs pybsposter-d9cc5878d-hht85
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://10.42.2.22:5000
Press CTRL+C to quit
10.42.0.20 - - [26/Nov/2024 13:35:35] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:35:35] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:13] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:15] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:16] "GET /server HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:16] "GET /version HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:16] "GET /.vscode/sftp.json HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:17] "GET /about HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:17] "GET /debug/default/view?panel=config HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:17] "GET /v2/_catalog HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:18] "GET /ecp/Current/exporttool/microsoft.exchange.ediscovery.exporttool.application HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:18] "GET /server-status HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:18] "GET /login.action HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:19] "GET /_all_dbs HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:19] "GET /.DS_Store HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:20] "GET /.env HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:20] "GET /.git/config HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:20] "GET /s/034323e2432323e23373e25373/_/;/META-INF/maven/com.atlassian.jira/jira-webapp-dist/pom.properties HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:21] "GET /config.json HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:21] "GET /telescope/requests HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:21] "GET /?rest_route=/wp/v2/users/ HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:36:25] "GET /favicon.ico HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:45] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:45] "GET /favicon.ico HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:47] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:47] "GET /favicon.ico HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:47] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:48] "GET /favicon.ico HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:52] "GET / HTTP/1.1" 404 -
10.42.0.20 - - [26/Nov/2024 13:37:53] "GET /favicon.ico HTTP/1.1" 404 -
[2024-11-26 13:40:59,734] ERROR in app: Exception on /post [POST]
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1511, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 919, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 917, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 902, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
  File "/app/app.py", line 20, in handle_post
    post = client.send_post(builder)
  File "/usr/local/lib/python3.9/site-packages/atproto_client/client/client.py", line 173, in send_post
    return self.app.bsky.feed.post.create(repo, record)
  File "/usr/local/lib/python3.9/site-packages/atproto_client/namespaces/sync_ns.py", line 788, in create
    response = self._client.invoke_procedure(
  File "/usr/local/lib/python3.9/site-packages/atproto_client/client/base.py", line 115, in invoke_procedure
    return self._invoke(InvokeType.PROCEDURE, url=self._build_url(nsid), params=params, data=data, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/atproto_client/client/client.py", line 41, in _invoke
    return super()._invoke(invoke_type, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/atproto_client/client/base.py", line 122, in _invoke
    return self.request.post(**kwargs)
  File "/usr/local/lib/python3.9/site-packages/atproto_client/request.py", line 165, in post
    return _parse_response(self._send_request('POST', *args, **kwargs))
  File "/usr/local/lib/python3.9/site-packages/atproto_client/request.py", line 155, in _send_request
    _handle_request_errors(e)
  File "/usr/local/lib/python3.9/site-packages/atproto_client/request.py", line 54, in _handle_request_errors
    raise exception
  File "/usr/local/lib/python3.9/site-packages/atproto_client/request.py", line 153, in _send_request
    return _handle_response(response)
  File "/usr/local/lib/python3.9/site-packages/atproto_client/request.py", line 79, in _handle_response
    raise exceptions.BadRequestError(error_response)
atproto_client.exceptions.BadRequestError: Response(success=False, status_code=400, content=XrpcError(error='InvalidRequest', message='Invalid app.bsky.feed.post record: Record/text must not be longer than 300 graphemes'), headers={'x-powered-by': 'Express', 'access-control-allow-origin': '*', 'cache-control': 'private', 'vary': 'Authorization, Accept-Encoding', 'ratelimit-limit': '5000', 'ratelimit-remaining': '4986', 'ratelimit-reset': '1732628466', 'ratelimit-policy': '5000;w=3600', 'content-type': 'application/json; charset=utf-8', 'content-length': '123', 'etag': 'W/"7b-5OYjOucrkx456vG6nIBiH/VZWIA"', 'date': 'Tue, 26 Nov 2024 13:40:59 GMT', 'keep-alive': 'timeout=90', 'strict-transport-security': 'max-age=63072000'})

We can see that “300” limit is hitting us again

Invalid app.bsky.feed.post record: Record/text must not be longer than 300 graphemes

Just to show that was the cause, I posted a revised entry I manually shortened.

$ cat bsky.payload2.json
{"USERNAME": "isaacj.bsky.social", "PASSWORD": "xxxxxxxxxxxxxx", "TEXT": "When one builds a poor man's data center in the basement with discarded and obsolete old tech, there are bound to be failures.  Today I wanted to share a few find-and-fix moments maintaining a functioning home lab.  From MinIO and NAS Updates to disk remediation...", "LINK": "https://go.tpk.pw/qzpq5"}

$ curl -X POST https://bskyposter.steeped.space/post -H 'Content-Type: application/json' -d @bsky.payload2.json
{"LINK":"https://go.tpk.pw/qzpq5","TEXT":"<atproto_client.utils.text_builder.TextBuilder object at 0x7f77b87d2a90>","YOU ARE:":"Isaac Johnson"}

which worked

/content/images/2024/12/bluesky-09.png

My Github Action should have handled this but didnt

      - name: Post to BlueSky
        run: | 
            # clean
            rm ./title.txt || true
            rm ./bsky.payload.json || true
            rm ./target.url || true
            rm ./yourls.output.json || true

            export LATESTFILE=`ls -l _posts/ | grep ".* 20[0-9][0-9]-[0-9][0-9].*markdown" | tail -n1 | sed 's/.* \(20[0-9][0-9]-[0-9][0-9].*markdown\)/\1/'`
            export LATESTURL=`ls -l _posts/ | grep ".* 20[0-9][0-9]-[0-9][0-9].*markdown" | tail -n1 | sed 's/.* \(20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-\)\(.*\)\.markdown/\2.html/'`
            cat _posts/$LATESTFILE | grep "^title: " | head -n 1 | sed 's/^title: "\(.*\)"/\1/' | tr -d '\n'> title.txt
            cat _posts/$LATESTFILE | grep "^social: " | head -n 1 | sed 's/^social: "\(.*\)"/\1/' | tr -d '\n'> social.txt
            
            # I suspect I need an extra space
            echo " " | tr -d '\n' >> social.txt

            char_count=$(wc -c ./social.txt)

            cat _posts/$LATESTFILE | grep "^date: " | head -n 1 | sed "s/^date: .\([0-9]*\)-\([0-9]*\)-\([0-9]*\) .*/https:\/\/freshbrewed.science\/\1\/\2\/\3\/$LATESTURL/g" | sed 's/.markdown/.html/' | tr -d '\n' > target.url
            curl --data-urlencode "url=`cat target.url`" --data-urlencode "title=`cat title.txt`" --data-urlencode 'format=json' --data-urlencode 'action=shorturl' --data-urlencode 'signature=6b15f178b9' "https://go.tpk.pw/yourls-api.php" | tee yourls.output.json
            cat yourls.output.json | jq -r '.shorturl' | tr -d '\n' > link.txt
            echo "================ bluesky ================"
            if [ "$char_count" -gt 275 ]; then
               echo "{\"USERNAME\": \"$BSKYUSER\", \"PASSWORD\": \"$BSKYPASS\", \"TEXT\": \"`head -c 275 ./social.txt`...\", \"LINK\": \"`cat ./link.txt`\"}" > bsky.payload.json
            else
               echo "{\"USERNAME\": \"$BSKYUSER\", \"PASSWORD\": \"$BSKYPASS\", \"TEXT\": \"`cat ./social.txt`\", \"LINK\": \"`cat ./link.txt`\"}" > bsky.payload.json
            fi

            # allow a way to not repost on image updates and hotfixes
            log=$(git log -n 1)
            
            set -x
            cat bsky.payload.json
            if [[ $log != *"SKIPSOCIAL"* ]]; then
              echo "================ now posting ================"
              curl -X POST https://bskyposter.steeped.space/post -H "Content-Type: application/json" -d @bsky.payload.json
            else 
              echo "CHECK-SKIP: Skip Posting To Social (BSky).. would have posted:"
              cat bsky.payload.json
            fi
        env:
          BSKYUSER: $
          BSKYPASS: $
          GHTOKEN: $

I’m realizing it would be far better to move the logic into the containerized app then try and keep fiddling with my Github actions

Updating the BS Poster App

I’ll clone it locally

builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/idjohnson/pybsposter.git
Cloning into 'pybsposter'...
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 9 (delta 0), reused 9 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (9/9), 2.69 KiB | 459.00 KiB/s, done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd pybsposter/

First, I don’t want to have to build the Docker container locally over and over.

Beyond that, Dockerhub’s rate limiting is getting really bad so I want to publish to Github CR or perhaps a public endpoing in my own CR.

Let’s look at the Github Workflow I just created in .github/workflows/container-build.yml

name: PR And Main Build
on:
  push:
    branches:
      - main
  pull_request:

  
jobs:
  cicd:
    runs-on: ubuntu-latest  
    steps:
      - run: echo "🎉 The job was automatically triggered by a $ event."
      - run: echo "🐧 This job is now running on a $ server hosted by GitHub!"
      - run: echo "🔎 The name of your branch is $ and your repository is $."
      - name: Check out repository code
        uses: actions/checkout@v2
      - 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: Check on Binaries
        run: |
          which az
          which aws
      - name: Build Dockerfile
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          docker build -t $BUILDIMGTAG .
          docker images
      - id: tagandpush
        name: Tag and Push
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`"
          docker tag $BUILDIMGTAG $FINALBUILDTAG
          docker images
          echo $CR_PAT | docker login harbor.freshbrewed.science -u $CR_USER --password-stdin
          docker push $FINALBUILDTAG
        env: # Or as an environment variable
          CR_PAT: $
          CR_USER: $
        if: github.ref == 'refs/heads/main'
      - id: tagnpushdry
        name: Tag and Push (DRY RUN)
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`"
          docker tag $BUILDIMGTAG $FINALBUILDTAG
          docker images
          echo $CR_PAT | docker login harbor.freshbrewed.science -u $CR_USER --password-stdin
          # SHOW THE COMMAND
          echo "IF we were on main, we would: docker push $FINALBUILDTAG"
        env: # Or as an environment variable
          CR_PAT: $
          CR_USER: $
        if: github.ref != 'refs/heads/main'
      - name: Build count
        uses: masci/datadog@v1
        with:
          api-key: $
          metrics: |
            - type: "count"
              name: "dockerbuild.runs.count"
              value: 1.0
              host: $
              tags:
                - "project:$"
                - "branch:$"
      - run: echo "🍏 This job's status is $."

Since I want to use my public CR, I’ll use the existing Forgejo user that has “developer” permissions to push containers there

/content/images/2024/12/bluesky-10.png

And I can the set my secrets, namely CR_PAT, CR_USER and DATADOG_API_KEY

/content/images/2024/12/bluesky-11.png

Lastly, before I test this flow, it assumes I put the primary destination CR as a comment in the last line of my Dockerfile. I’ll parse the version from here (it allows builds this way)

# Use an official Python runtime as a parent image
FROM python:3.9-slim

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
RUN pip install -r requirements.txt

# Make port 5000 available to the world outside this container
EXPOSE 5000

# Define environment variable
ENV NAME=World

# Run app.py when the container launches
CMD ["python", "app.py"]

#harbor.freshbrewed.science/library/pybsposter:0.0.1

Time to see if it works

builder@DESKTOP-QADGF36:~/Workspaces/pyBSPoster$ git commit -m "Build File"
[main f2bee6c] Build File
 2 files changed, 70 insertions(+)
 create mode 100644 .github/workflows/container-build.yml
builder@DESKTOP-QADGF36:~/Workspaces/pyBSPoster$ git push
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 16 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 1.35 KiB | 1.35 MiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/idjohnson/pybsposter.git
   9c34f95..f2bee6c  main -> main

I can see it fired off a workflow action

/content/images/2024/12/bluesky-12.png

I can now see the pull command

/content/images/2024/12/bluesky-13.png

docker pull harbor.freshbrewed.science/library/pybsposter@sha256:706b07f0f80917fa35577d3ba80e9053b225e2d4604a1374480aee8eb69aeaee

or

docker pull harbor.freshbrewed.science/library/pybsposter:0.0.1

I’ll test by editting my deployment to use the image

$ kubectl get deployment pybsposter -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"pybsposter","namespace":"default"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"pybsposter"}},"template":{"metadata":{"labels":{"app":"pybsposter"}},"spec":{"containers":[{"image":"idjohnson/pybsposter:latest","name":"pybsposter","ports":[{"containerPort":5000}]}]}}}}
  creationTimestamp: "2024-11-26T13:31:09Z"
  generation: 1
  name: pybsposter
  namespace: default
  resourceVersion: "43494053"
  uid: 10985d02-0bbd-43f4-aed7-12ca509a307c
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: pybsposter
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: pybsposter
    spec:
      containers:
      - image: idjohnson/pybsposter:latest
        imagePullPolicy: Always
        name: pybsposter
        ports:
        - containerPort: 5000
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status:
  availableReplicas: 1
  conditions:
  - lastTransitionTime: "2024-11-26T13:31:15Z"
    lastUpdateTime: "2024-11-26T13:31:15Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  - lastTransitionTime: "2024-11-26T13:31:09Z"
    lastUpdateTime: "2024-11-26T13:31:15Z"
    message: ReplicaSet "pybsposter-d9cc5878d" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  observedGeneration: 1
  readyReplicas: 1
  replicas: 1
  updatedReplicas: 1

$ kubectl edit deployment pybsposter 
$ kubectl get deployment pybsposter -o yaml | grep 'image:'
      - image: harbor.freshbrewed.science/library/pybsposter:0.0.1

And i can see the rotated pod is using the public image

builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ kubectl get pods | grep pybs
pybsposter-5f6667dc8c-269rh                          1/1     Running     0                60s
builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ kubectl get pods pybsposter-5f6667dc8c-269rh -o yaml | grep -i image:
  - image: harbor.freshbrewed.science/library/pybsposter:0.0.1
    image: harbor.freshbrewed.science/library/pybsposter:0.0.1

I used Copilot and Gemini Code Assist and they both suggested the same updated code:

    # Calculate the total length of text and link 
    total_length = len(text) + len(link)
    if total_length > 300:
        text = text[:(300 - len(link))]

/content/images/2024/12/bluesky-14.png

You can see these next changes which include adding a “latest” tag, updated notes in the README.md and deployment.yaml

/content/images/2024/12/bluesky-15.png

I can see both tags were pushed

/content/images/2024/12/bluesky-16.png

I’ll now update the Github Workflow to build and push a helm chart:

name: PR And Main Build
on:
  push:
    branches:
      - main
  pull_request:

  
jobs:
  cicd:
    runs-on: ubuntu-latest  
    steps:
      - run: echo "🎉 The job was automatically triggered by a $ event."
      - run: echo "🐧 This job is now running on a $ server hosted by GitHub!"
      - run: echo "🔎 The name of your branch is $ and your repository is $."
      - name: Check out repository code
        uses: actions/checkout@v2
      - 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: Build Dockerfile
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          docker build -t $BUILDIMGTAG .
          docker images
          # Package charts
          cd charts
          helm package pybsposter
      - id: tagandpush
        name: Tag and Push
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`"
          export FINALBUILDLATEST="`cat Dockerfile | tail -n1 | sed 's/^#//g' | sed 's/:.*/:latest/'`"
          docker tag $BUILDIMGTAG $FINALBUILDTAG
          docker tag $BUILDIMGTAG $FINALBUILDLATEST
          docker images
          echo $CR_PAT | docker login harbor.freshbrewed.science -u $CR_USER --password-stdin
          docker push $FINALBUILDTAG
          # add a "latest" for others to use
          docker push $FINALBUILDLATEST
          # Push Charts
          export CVER="`cat ./charts/pybsposter/Chart.yaml | grep 'version:' | sed 's/version: //' | tr -d '\n'`"
          helm push ./charts/pybsposter-$CVER.tgz oci://harbor.freshbrewed.science/library/
        env: # Or as an environment variable
          CR_PAT: $
          CR_USER: $
        if: github.ref == 'refs/heads/main'
      - id: tagnpushdry
        name: Tag and Push (DRY RUN)
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`"
          docker tag $BUILDIMGTAG $FINALBUILDTAG
          docker images
          echo $CR_PAT | docker login harbor.freshbrewed.science -u $CR_USER --password-stdin
          # SHOW THE COMMAND
          echo "IF we were on main, we would: docker push $FINALBUILDTAG"
        env: # Or as an environment variable
          CR_PAT: $
          CR_USER: $
        if: github.ref != 'refs/heads/main'
      - name: Build count
        uses: masci/datadog@v1
        with:
          api-key: $
          metrics: |
            - type: "count"
              name: "dockerbuild.runs.count"
              value: 1.0
              host: $
              tags:
                - "project:$"
                - "branch:$"
      - run: echo "🍏 This job's status is $."

I won’t list out the charts, but you can see them here

And I can Github Actions workflow worked:

/content/images/2024/12/bluesky-17.png

One last change - if I plan to share this, ingress should be disabled by default.

builder@DESKTOP-QADGF36:~/Workspaces/pyBSPoster$ git diff
diff --git a/charts/pybsposter/Chart.yaml b/charts/pybsposter/Chart.yaml
index 0b13eae..c7a737a 100644
--- a/charts/pybsposter/Chart.yaml
+++ b/charts/pybsposter/Chart.yaml
@@ -2,6 +2,6 @@ apiVersion: v2
 name: pybsposter
 description: A Helm chart for deploying the pybsposter application
 type: application
-version: 0.1.0
+version: 0.1.1
 appVersion: "1.0"
 
diff --git a/charts/pybsposter/values.yaml b/charts/pybsposter/values.yaml
index 604ec99..bdbc378 100644
--- a/charts/pybsposter/values.yaml
+++ b/charts/pybsposter/values.yaml
@@ -8,7 +8,7 @@ service:
   targetPort: 5000
   type: ClusterIP # Or LoadBalancer
 ingress:
-  enabled: true  # To enable or disable the ingress
+  enabled: false  # To enable or disable the ingress
   host: bskyposter.steeped.space
   tlsSecretName: pybspostergcp-tls
   ingressClass: nginx

I can now test by using the OCI URL

$ helm delete pybsposter
$ helm install pybsposter oci://harbor.freshbrewed.science/library/pybsposter --version 0.1.1
Pulled: harbor.freshbrewed.science/library/pybsposter:0.1.1
Digest: sha256:2b02bef80dba8c450c2cb59df7415e93eba439f97f264c6d4e8a28687f6d6796
NAME: pybsposter
LAST DEPLOYED: Fri Dec  6 08:03:53 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

Did it make an ingress?

$ kubectl get ingress | grep pybs
$

Now let’s update to enable the ingress

$ helm upgrade pybsposter oci://harbor.freshbrewed.science/library/pybsposter --version 0.1.1 --set ingress.enabled=true
Pulled: harbor.freshbrewed.science/library/pybsposter:0.1.1
Digest: sha256:2b02bef80dba8c450c2cb59df7415e93eba439f97f264c6d4e8a28687f6d6796
Release "pybsposter" has been upgraded. Happy Helming!
NAME: pybsposter
LAST DEPLOYED: Fri Dec  6 08:10:54 2024
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None

Testing

Let’s use a longer post. I was a bit wordy just see a content I know would exceed the limit

$ cat /home/builder/Workspaces/jekyll-blog/bsky.payload2.json
{"USERNAME": "isaacj.bsky.social", "PASSWORD": "xxxxxxxxx", "TEXT": "Looking forward to taking the kiddos to the St. Paul Ice Fishing Show today (or this weekend).  I keep checking the temps and I hope to get down to the local small lake to test the thickness. I tend to use a pop-up insulated tent but one of these years I hope to get a proper ice-house", "LINK": "https://www.stpaulicefishingshow.com/"}

$ curl -X POST https://bskyposter.steeped.space/post -H 'Content-Type: application/json' -d @/home/builder/Workspaces/jekyll-blog/bsky.payload2.json
{"LINK":"https://www.stpaulicefishingshow.com/","TEXT":"<atproto_client.utils.text_builder.TextBuilder object at 0x7f7b8b0ccb80>","YOU ARE:":"Isaac Johnson"}

That worked, but I wonder if I should add ellipses?

/content/images/2024/12/bluesky-18.png

Can I just take a moment to say how much I like Gemini Code Assist? I asked to add the “…” and I planned to account for the extra space by dropping 300 to 297 but I noticed it already accounted for that when it returned the updated Python code

/content/images/2024/12/bluesky-19.png

I’ll update for that (and add a space)

uilder@DESKTOP-QADGF36:~/Workspaces/pyBSPoster$ git diff
diff --git a/Dockerfile b/Dockerfile
index c04924b..98340c7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -19,4 +19,4 @@ ENV NAME=World
 # Run app.py when the container launches
 CMD ["python", "app.py"]
 
-#harbor.freshbrewed.science/library/pybsposter:0.0.2
\ No newline at end of file
+#harbor.freshbrewed.science/library/pybsposter:0.0.4
\ No newline at end of file
diff --git a/app.py b/app.py
index 1cf719d..449f4ad 100644
--- a/app.py
+++ b/app.py
@@ -15,8 +15,8 @@ def handle_post():
     # Calculate the total length of text and link 
     total_length = len(text) + len(link)
     if total_length > 300:
-        text = text[:(300 - len(link))]
-        
+        text = text[:(300 - len(link) - 4)] + "... "  # Trim and add ellipsis
+
     client = Client()
     profile = client.login(username, password)

and push

builder@DESKTOP-QADGF36:~/Workspaces/pyBSPoster$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/pyBSPoster$ git commit -m "update app to 0.0.3 and change the code to add an elipse when trimming"
[main e1d78e5] update app to 0.0.3 and change the code to add an elipse when trimming
 2 files changed, 3 insertions(+), 3 deletions(-)
builder@DESKTOP-QADGF36:~/Workspaces/pyBSPoster$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 16 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 433 bytes | 433.00 KiB/s, done.
Total 4 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
To https://github.com/idjohnson/pybsposter.git
   3faa2c1..e1d78e5  main -> main

Since my chart uses the image pull policy of “IfNotPresent” and a tag of “latest”, I’ll want to force it to use a versioned tag to force it to update now

$ helm upgrade pybsposter oci://harbor.freshbrewed.science/library/pybsposter --version 0.1.1 --set ingress.enabled=true --set image.tag=0.0.4
Pulled: harbor.freshbrewed.science/library/pybsposter:0.1.1
Digest: sha256:7d4a74776340b1cdc27c8c757ce0513687ca0865459f65dea67e9edeede9479b
Release "pybsposter" has been upgraded. Happy Helming!
NAME: pybsposter
LAST DEPLOYED: Fri Dec  6 08:18:52 2024
NAMESPACE: default
STATUS: deployed
REVISION: 3
TEST SUITE: None

A delete of the old post and retry

$ curl -X POST https://bskyposter.steeped.space/post -H 'Content-Type: application/json' -d @/home/builder/Workspaces/jekyll-blog/bsky.payload2.json
{"LINK":"https://www.stpaulicefishingshow.com/","TEXT":"<atproto_client.utils.text_builder.TextBuilder object at 0x7fb66b126c10>","YOU ARE:":"Isaac Johnson"}

/content/images/2024/12/bluesky-20.png

I’m not going to edit it further, even if it seems odd to trim a status update.

Summary

We updated the BlueSky app to use a Helm Chart and fixed it to automatically trim larger posts. This should prevent our Github Posts from ever dropping due to size issues.

We also changed the Github repo to use Github Actions and push to my self-hosted Harbor CR in a public share (as well as an OCI Helm chart). Thus anyone can now helm install with just this one-liner:

$ helm install pybsposter oci://harbor.freshbrewed.science/library/pybsposter --version 0.1.1

I just overrode some values for my local install

$ helm get values pybsposter
USER-SUPPLIED VALUES:
image:
  tag: 0.0.4
ingress:
  enabled: true

But all the values

$ helm get values --all pybsposter
COMPUTED VALUES:
image:
  pullPolicy: IfNotPresent
  repository: harbor.freshbrewed.science/library/pybsposter
  tag: 0.0.4
ingress:
  certManagerIssuer: gcpleprod2
  clientMaxBodySize: "0"
  connectTimeout: "3600"
  enabled: true
  host: bskyposter.steeped.space
  ingressClass: nginx
  proxyBodySize: "0"
  readTimeout: "3600"
  sendTimeout: "3600"
  sslRedirect: "true"
  tlsAcme: "true"
  tlsSecretName: pybspostergcp-tls
replicas: 1
service:
  port: 80
  targetPort: 5000
  type: ClusterIP
OpenSource BlueSky Python Github DevOps CICD

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