Try to Hack My AI-Agent Workflow: GitHub Issues as a Control Surface

Two possible outcomes of the public challenge

I built a small public experiment.

You can open a GitHub Issue and ask an AI-agent workflow to create a static page on my website:

  1. If your request fits the rules, the workflow may turn it into a published page.
  2. If your request does not fit the rules, the agents should reject it and explain why.

And if you want to make the experiment more interesting, do not only submit a valid request. Try to bypass the boundary. Try to make the workflow publish something it should not publish. Then inspect the issue comments, labels, pull request, and final result.

This is the game.

But under that game, there is a more serious question:

Can external user intent enter a real product repository and become a bounded, reviewable product change without giving up product ownership?

That is what I am trying to explore.

The public playground is here: Demo Pages

The challenge

The surface rule is simple:

Create a GitHub Issue that requests a small static demo page.

The workflow should read the issue, decide whether the request belongs to the allowed area, run the required agent steps, create or update the page, validate the result, and publish it only if the process succeeds.

But this is not a public CMS.

It is not a hosting service.

It is not a website builder.

It is not a place where an external user should be able to change my homepage, global layout, styles, scripts, product pages, or site structure.

The allowed result is intentionally narrow: a bounded inert static page under the Demo Pages section.

So the challenge has two sides.

First, try the normal path:

Create a small demo page request and see whether the workflow can turn it into a published static artifact.

Then try the adversarial path:

Ask for something outside the boundary and see whether the workflow stops you.

For example, you can try to request a homepage change, inject instructions into the issue body, ask the agent to modify global styles, or try to smuggle forbidden behavior into the generated content.

Please keep the challenge inside the intended boundary. This is a workflow and prompt-boundary experiment, not an infrastructure attack. Do not attack the server, credentials, network, GitHub itself, availability, or anything outside the issue text and the public repository workflow.

The interesting question is not whether the server can be broken.

The interesting question is whether a chain of AI agents can defend a product boundary while still allowing useful delegated work.

Two possible outcomes of the public challenge

What you can inspect

The workflow is intentionally public enough to leave evidence.

A request should not disappear into a private chatbot.

A useful result should leave a visible trail:

  • the original issue;
  • the labels that describe state transitions;
  • comments written by agents;
  • a pull request when a change is produced;
  • validation evidence;
  • the published static page when the change is accepted.

The page itself is not the main artifact.

The main artifact is the trace.

A static page is only the visible end of the process. The more important part is the path from external request to controlled product change.

If the workflow accepts your request, you can inspect what happened.

If the workflow rejects your request, you can inspect why.

A successful rejection is also a useful result.

In ordinary automation, a rejection often feels like failure. In an AI-agent workflow, a correct rejection is evidence that the process has boundaries. It means the agent did not blindly satisfy the user request. It treated the issue as a signal, not as a command.

That distinction matters.

Why I built this

AI coding agents are getting better at producing code.

But code generation is not the hardest part of using agents in real products.

The harder problem is control.

  • Who allowed the agent to work?
  • What kind of request was considered safe?
  • Which files was the agent allowed to change?
  • What checks were required?
  • What evidence was recorded?
  • When should the agent stop and ask for a human decision?

If these questions are not answered, “autonomous agent development” easily becomes an uncontrolled prompt playground.

That may be interesting for demos, but it is not how I want agents to touch real products.

In a product environment, the owner must not lose control. Users can express intent. Agents can perform delegated work. But the product owner must define the paths where autonomous work is allowed, the boundaries where it must stop, and the evidence required before the result enters the product.

This experiment uses static demo pages because they are safe, visible, and easy to understand.

But the real subject is not static page generation.

The real subject is controlled product evolution.

A user opens an issue. The workflow checks whether the request belongs to an approved path. If it does, agents may process it. If it does not, the request should be rejected, blocked, or routed to human review.

That is the model I want to test in public.

The workflow behind the game

The current experiment uses GitHub Issues as the entry point.

A GitHub Issue is a familiar development artifact. It can describe a request, collect discussion, receive labels, trigger events, and preserve history. That makes it a practical control surface for agent-driven work.

The simplified route looks like this:

Workflow route from GitHub Issue to published demo page

The workflow does not treat the issue as a direct publishing request.

It treats the issue as a development signal.

That signal must pass through rules.

For the Demo Pages experiment, the allowed area is intentionally small. The agent may work only inside the documented demo-page boundary. It must not turn a reader request into arbitrary site changes.

This is where the workflow becomes more important than the generated page.

A simple page-generation demo could be built with one prompt and one script.

That is not the point here.

The point is to show a controlled route where request admission, agent execution, validation, pull request creation, merge, and publication are separate observable steps.

What protects the boundary

The boundary is not protected by asking the model to “please behave.”

That is not enough.

The workflow needs several kinds of control.

First, there is a documented scope.

The agent must know what kind of request is allowed, what area of the repository may be changed, and what must remain untouched.

Second, there is admission.

Before implementation starts, the request should be classified. A request that targets the homepage, global layout, scripts, styles, or product content should not be treated as a valid demo-page request.

Third, there is restricted execution.

Agents should run in a bounded environment and work through repository changes, not through direct production mutation.

Fourth, there is validation.

The result must be checked before it becomes a published artifact.

Fifth, there is evidence.

Labels, comments, pull requests, and logs matter because they make the process inspectable. Without evidence, autonomy is difficult to trust.

This is why I am inviting people not only to submit valid requests, but also to submit boundary-crossing requests.

A workflow that only succeeds on clean examples is not very interesting.

A workflow becomes more useful when it can say:

No, this request is outside the approved path, and here is why.

That answer is part of the product control model.

Why this may matter for product teams

This experiment looks small because the artifact is small.

A static page is not a large feature.

But the pattern is larger than the artifact.

Many teams already use GitHub Issues, pull requests, CI checks, reviews, and deployment pipelines. AI agents can enter this environment, but they need more than task prompts. They need process boundaries.

A product team does not only need an agent that can write code.

It needs a way to answer:

  • which requests can be delegated;
  • which requests require human review;
  • what evidence must be produced;
  • what validation is required;
  • how the owner can inspect and change the rules;
  • how the product avoids becoming controlled by random external prompts.

That is the commercial reason I care about this.

I am not trying to demonstrate that an AI agent can write a static page. That is already obvious.

I am trying to demonstrate that an AI-agent workflow can transform an external request into a controlled, reviewable, bounded product change.

For a real team, this pattern could apply to documentation fixes, small UI corrections, test additions, generated examples, controlled content updates, internal tools, or other well-bounded change paths.

The important part is not the specific artifact.

The important part is the governance loop:

intent -> boundary check -> delegated work -> evidence -> review -> product change

What powers this experiment

The public workflow is powered by my GitHub Flows work.

The host application is here: github-flows-app

It is a ready-to-run host application around @teqfw/github-flows. It starts a Node.js process, loads local runtime configuration, exposes the GitHub webhook ingress, provides a workspace for configuration and logs, and allows the workflow runtime to react to GitHub events.

The host application itself is not the whole idea.

The broader idea is that repository events can drive agent execution through an explicit process. An issue is opened. A label is added. A pull request is created. A validation result appears. A merge happens. These events can move work through a controlled chain instead of relying on one long manual chat session.

I described the conceptual layer here: Turning User Intent Into Controlled Product Evolution

That article explains the model behind this experiment: users express intent, the product owner defines approved paths, agents act as delegated intermediaries, and the product changes only through bounded, observable, reviewable steps.

Try it

Start here: Demo Pages

Try a simple valid request first.

Then try a request that should be rejected.

Look at the labels.

Read the agent comments.

Follow the pull request if one appears.

Open the published page if the request reaches publication.

If the workflow rejects you, that may be the correct result.

If the workflow publishes something it should not publish, I want to know.

And if you are working with a real product team and thinking about how AI agents could safely participate in GitHub-based development, I am interested in practical cases.

The question is no longer only:

Can an AI agent write code?

The more useful question is:

Can we give agents limited authority inside a process that product owners can control?

If that is the kind of problem you are facing, you can contact me.

# I built a type-safe SQL library for Bun — no ORM, no codegen, just SQL (using Claude Code)

I’ve been using Bun for a while and kept running into the same problem: every SQL library either requires Node.js internals, leans heavily on an ORM abstraction I don’t want, or generates types from a schema file at build time.

So I built squn — a lightweight, type-safe SQL query library that works natively with Bun’s built-in database clients.

The core idea

Every query goes through a tagged template literal called sql. Interpolated values always become bound parameters — they are never concatenated into the SQL string. SQL injection is structurally impossible by design.

import { createDb, PostgresAdapter, sql } from "@phonemyatt/squn";

const db = createDb(new PostgresAdapter({
  url: "postgresql://user:password@localhost:5432/mydb",
}));

interface User {
  id: number;
  name: string;
  age: number | null;
}

// Values become $1, $2 parameters — never string-concatenated
const users = await db.query<User>(sql`SELECT * FROM users WHERE age > ${18}`);

No schema file. No code generation step. No build-time magic. You write SQL, get back typed results.

Four databases, one API

squn supports all four databases you’re likely to use with Bun:

Database Driver
SQLite bun:sqlite (built-in)
PostgreSQL Bun’s native Postgres
MySQL Bun’s native MySQL
MSSQL mssql npm package

The same query code works across all four — only the adapter construction changes.

// Switch databases by swapping the adapter
const db = createDb(new SqliteAdapter({ filename: ":memory:" }));
const db = createDb(new PostgresAdapter({ url: process.env.PG_URL }));
const db = createDb(new MysqlAdapter({ url: process.env.MYSQL_URL }));
const db = createDb(new MssqlAdapter({ host: "localhost", ... }));

Query methods that match what you actually need

// All rows
const users = await db.query<User>(sql`SELECT * FROM users`);

// First row or null — no throw
const user = await db.queryFirst<User>(sql`SELECT * FROM users WHERE id = ${1}`);

// Exactly one row — throws if 0 or 2+ rows returned
const user = await db.querySingle<User>(sql`SELECT * FROM users WHERE id = ${1}`);

// Scalar — first column of first row
const count = await db.queryScalar<number>(sql`SELECT COUNT(*) FROM users`);

Composable SQL fragments

Fragments compose. Nested fragments merge inline and placeholders are renumbered automatically.

const minAge = 18;
const activeOnly = true;

const conditions = [
  sqlIf(minAge !== undefined, sql`age >= ${minAge}`),
  sqlIf(activeOnly, sql`active = ${true}`),
];

const where = sqlJoin(conditions, " AND ");
const q = sql`SELECT * FROM users WHERE ${where} ORDER BY name`;
// → SELECT * FROM users WHERE age >= $1 AND active = $2 ORDER BY name
// params → [18, true]

No string concatenation. No injection risk. Full composability.

Transactions that don’t leak

atomically wraps your callback in BEGIN/COMMIT and rolls back automatically on error:

await db.atomically(async (q) => {
  await q.execute(sql`UPDATE accounts SET balance = balance - ${100} WHERE id = ${from}`);
  await q.execute(sql`UPDATE accounts SET balance = balance + ${100} WHERE id = ${to}`);
  // if either throws, both updates are rolled back
});

Transaction also implements Symbol.asyncDispose — so await using gives you guaranteed cleanup:

await using tx = new Transaction(await adapter.beginTransaction());
await tx.execute(sql`UPDATE users SET active = ${false} WHERE id = ${42}`);
await tx.commit();
// if commit throws or you return early, rollback happens automatically

Batch inserts with a single prepared statement

await db.executeBatch(
  sql`INSERT INTO users (name, age) VALUES (@name, @age)`,
  [
    { name: "Alice", age: 30 },
    { name: "Bob",   age: 25 },
    { name: "Carol", age: 35 },
  ],
);

One prepared statement, all rows bound in a loop. Much faster than individual inserts.

Type inference from table definitions

Define your table schema once, get insert/select/update types inferred automatically:

import { col, defineTable, InferSelect, InferInsert } from "@phonemyatt/squn";

const Users = defineTable({
  id:   col("integer").primaryKey().notNull(),
  name: col("text").notNull(),
  age:  col("integer").nullable(),
});

type UserRow    = InferSelect<typeof Users>;  // { id: number; name: string; age: number | null }
type UserInsert = InferInsert<typeof Users>;  // { name: string; age?: number | null }

Multi-connection and read replicas

const db = createConnections({
  connections: {
    primary: new PostgresAdapter({ url: process.env.PRIMARY }),
    replica: new PostgresAdapter({ url: process.env.REPLICA }),
  },
  default: "primary",
});

// Route reads to replica
const users = await db.query<User>(sql`SELECT * FROM users`, { connection: "replica" });

// Scoped helper — no connection option needed per call
const replica = db.use("replica");

// Typed concurrent queries
const [users, roles] = await db.concurrent(
  db.query<User>(sql`SELECT * FROM users`),
  db.query<Role>(sql`SELECT * FROM roles`),
);

Try it

bun add @phonemyatt/squn

Feedback welcome — especially from anyone using it with MySQL or MSSQL in production.

Built with TypeScript 5.9 strict mode, zero any, and tested against real databases in Docker.

DynamoDB Contributor Insights Pricing: What You’re Actually Paying For

/$0.50 per rule per month. $0.03 per million events. Enabling Contributor Insights on a single DynamoDB table auto-creates four rules so your floor is $2.00/month before a single event is counted.

At 500 million events/month, that’s $17.00/month. At 5 billion, it’s $152.00/month.

Whether that’s justified depends on your traffic volume, whether you have hot key patterns, and what throttling is already costing you in wasted provisioned throughput.

What Is DynamoDB Contributor Insights?

Contributor Insights for DynamoDB is a CloudWatch monitoring feature that identifies the top partition keys driving read and write traffic on a table without any code-level instrumentation or custom application logging.

It analyzes request traffic in near real-time and publishes results as CloudWatch graphs. For tables with partition key skew, it answers the specific question a FinOps engineer needs: “Which partition key is causing throttling?”

The Two Pricing Components

Contributor Insights billing has exactly two line items on your CloudWatch invoice:

  • Rule fee: $0.50 per rule per month, flat regardless of traffic
  • Event fee: $0.03 per 1 million DynamoDB read/write events matched

How many rules does one table create?

Enabling on a single table auto-creates four rules:

  • Most-accessed partition keys on the base table
  • Most-accessed partition keys causing errors on the base table
  • Most-accessed partition keys on each GSI
  • Most-accessed partition keys causing errors on each GSI

Rule count formula: 2 + (2 × number of GSIs)

A table with no GSIs = $2.00/month in rule fees. A table with 3 GSIs = $4.00/month in rule fees, before any events.

What counts as an “event”?

Every read and write operation: GetItem, PutItem, UpdateItem, DeleteItem, BatchGetItem, BatchWriteItem (each item individually), Query and Scan (each item returned or examined), and transactional reads/writes. GSI operations are counted separately when rules are active on both.

Worked Cost Examples

10M events/month: $2.00 rule fee + $0.30 event fee = $2.30/month

500M events/month: $2.00 rule fee + $15.00 event fee = $17.00/month

2B events/month: $2.00 rule fee + $60.00 event fee = $62.00/month

2B events/month + 3 GSIs: $4.00 rule fee + $60.00 event fee = $64.00/month

For most workloads, the event fee is the variable that matters. The rule fee is fixed overhead.

Throttle-Only Mode

As of August 2025, AWS added a throttle-only mode. Rules only activate for events that result in throttling requests that exceed provisioned capacity or on-demand burst limits.

You still pay the $0.50/rule/month rule fee. But the event fee drops dramatically. On a well-provisioned table with a 0.1% throttle rate processing 2 billion events/month, you’d match roughly 2 million throttled events, an event cost of $0.06/month instead of $60.00/month.

Use throttle-only mode when:

  • You only need to diagnose which keys are causing throttling, not which are most popular
  • Your table is currently healthy and you want passive monitoring with near-zero cost
  • You have a high-volume table (1B+ events/month) where full mode runs $30+/month

Use full mode when:

  • You’re actively investigating a hot key pattern
  • You want continuous visibility into traffic distribution
  • You’re evaluating whether to restructure your partition key schema

If you’re deciding between 1-year and 3-year AWS commitment terms before locking in Reserved Capacity, the tradeoffs are covered in detail here How to Choose Between 1-Year and 3-Year AWS Commitments

The Real Cost Question: What Does Hot Key Blindness Cost?

Contributor Insights costs $2 to $62/month. The cost of not having it shows up in your provisioned capacity bill every month.

DynamoDB provisioned capacity is distributed across partitions. If your table is configured for 10,000 RCUs/second and 90% of reads go to three partition keys, the capacity on your other partitions sits idle while those three keys throttle.

Common patterns where this happens:

  • Low-cardinality partition keys (e.g., user_type, region, status)
  • Popular cached items that expire simultaneously, causing a read surge on one key
  • Time-based queries where all traffic clusters around “today” or “current”

A concrete scenario: a table provisioned at 5,000 WCUs/second costs approximately $2,847/month in write capacity (verify current rates at aws.amazon.com/dynamodb/pricing). If hot key throttling means 30% of that capacity is sitting idle on underutilized partitions, you’re paying ~$854/month for throughput you can’t use.

Contributor Insights at $17/month to identify which keys to redesign around: that’s a 50:1 return in the month you act on it.

*Hot Keys and Reserved Capacity Commitments
*

This is where hot key detection connects directly to cost optimization strategy.

DynamoDB Reserved Capacity delivers 30–40% savings on provisioned throughput. But those savings assume your capacity is being utilized effectively. A table with undetected hot key patterns has two compounding problems:

  • It’s over-provisioned because teams add capacity to compensate for throttling instead of fixing the partition key design
  • When you commit to Reserved Capacity on an over-provisioned table, you lock in the waste

The right sequence: identify hot keys via Contributor Insights → fix the partition key schema → right-size provisioned capacity → apply Reserved Capacity on that right-sized number. The result is a smaller commitment with higher utilization and 30–40% off the correct baseline.

Contributor Insights vs Standard CloudWatch Metrics

CloudWatch already provides DynamoDB metrics by default, at no charge. The key difference is specificity.

Standard CloudWatch metrics tell you “you have 50,000 throttled requests this hour.” Contributor Insights tells you “partition key user#12345 accounts for 48,000 of those throttled requests.”

For active troubleshooting or capacity planning, that distinction is the difference between guessing and acting.

When to Enable It

Enable Contributor Insights when:

  • Your table experiences recurring throttling that standard metrics can’t explain
  • You’re planning a table migration or partition key schema redesign
  • Your table processes 100M+ events/month and you have no visibility into traffic distribution
  • You’re considering a Reserved Capacity commitment and want to validate utilization first

Use throttle-only mode when:

  • You want passive monitoring with near-zero event costs
  • Your table is healthy but you want early warning if hot keys develop

Skip it entirely when:

  • You have very high cardinality partition keys (e.g., UUID-based) with no known skew
  • You’re on on-demand capacity mode with confirmed even traffic distribution
  • The table is non-production with no Reserved Capacity commitment

How to Enable It

Prerequisites: IAM permissions for cloudwatch:EnableInsightRules and dynamodb:DescribeTable. Understand the event cost for your traffic volume before enabling full mode.

  • Open the DynamoDB console and select your target table
  • Go to the Monitor tab
  • Click Manage Contributor Insights; select “All Requests” (full mode) or “Throttled Requests Only” (throttle-only mode)
  • Navigate to CloudWatch > Contributor Insights and confirm the rules are in “Enabled” state
  • Allow 15–30 minutes for data to appear; CloudWatch will show the top 10 partition keys by request volume

If rules show an “Error” state, the most common cause is insufficient IAM permissions. Verify the execution role includes cloudwatch:EnableInsightRules, cloudwatch:PutInsightRule, and dynamodb:DescribeTable.

How This Fits With Reserved Capacity

Identifying hot keys is a prerequisite to maximizing Reserved Capacity ROI. A table with hot key throttling shows a deceptive utilization signal: total consumed capacity may look high, but actual table-level efficiency is low because hot partitions throttle while cold partitions sit idle.

Usage.ai’s Flex Reserved Instances handle DynamoDB Reserved Capacity commitments with no upfront payment, no multi-year lock-in, and a cashback guarantee if utilization falls short see how it works at usage.ai

Usage.ai refreshes DynamoDB recommendations every 24 hours, vs. the 72+ hour refresh cycle on AWS native tools. At DynamoDB pricing, that 3-day gap in detecting newly-emerged hot key patterns can represent meaningful daily waste before any action is taken.

Have you run into hot key issues on DynamoDB? Did you catch it via Contributor Insights or some other way and what did the fix end up looking like?

Explore the full operational breakdown here → DynamoDB Contributor Insights Pricing: Monitoring Hot Keys at a Cost

Cloudflare Completes Its Agent Infrastructure Stack with Browser Run Rebuild and Six-Layer Platform

Cloudflare rebuilt Browser Run on its own Containers platform, delivering 4x higher concurrency and 50% faster response times. The upgrade completes a six-layer agent infrastructure stack: compute (Dynamic Workers + Sandboxes), orchestration (Dynamic Workflows), memory (Agent Memory), browsing (Browser Run), and commerce (Stripe Projects).

By Steef-Jan Wiggers

Oracle Linux 7 to 8 Upgrade Using Leapp: Architecture, Inhibitors, and Enterprise Troubleshooting

Introduction

Enterprise Linux operating systems require periodic upgrades to maintain security, supportability, compliance, and operational stability.

As organizations modernize infrastructure platforms, migrating from Oracle Linux 7 to Oracle Linux 8 becomes important because Oracle Linux 8 introduces:

✔ Modern package management
✔ Improved security
✔ Better kernel support
✔ Enhanced automation compatibility
✔ AppStream modular repositories
✔ Long-term enterprise support

However, major Linux upgrades are not simple package updates.

They involve:

✔ Repository transitions
✔ Package dependency changes
✔ Kernel migration
✔ Bootloader modifications
✔ Service compatibility validation
✔ Third-party package handling

Oracle Linux provides the Leapp upgrade utility to automate and orchestrate Oracle Linux 7 to Oracle Linux 8 migrations safely.

In this blog, we will perform a complete deep dive into:

✔ Configuring Leapp repositories
✔ Installing Leapp utility
✔ Preupgrade analysis
✔ Understanding inhibitors
✔ Answer file handling
✔ Repository migration
✔ Upgrade execution
✔ Upgrade boot workflow
✔ Enterprise troubleshooting
✔ Real-world operational challenges

Understanding Leapp

Leapp is an in-place upgrade utility used to migrate Oracle Linux 7 systems to Oracle Linux 8.

Why Major OS Upgrades Are Complex

Upgrading from Oracle Linux 7 to Oracle Linux 8 involves platform-level architectural changes.

These include:

✔ Kernel transitions
✔ Package replacement
✔ Repository mapping
✔ Driver compatibility
✔ Service migration
✔ Security policy updates
✔ Boot environment changes

Note : Operating system upgrades are not only package upgrades — they are full platform transitions.

Oracle Linux Upgrade Architecture

The Leapp upgrade process follows multiple operational stages.

Upgrade Workflow

Oracle Linux 7
│
▼
Repository Validation
│
▼
Leapp Installation
│
▼
Preupgrade Analysis
│
▼
Inhibitor Detection
│
▼
Answer File Validation
│
▼
Upgrade Initramfs Creation
│
▼
System Reboot
│
▼
Upgrade Environment Boot
│
▼
Package Migration
│
▼
Oracle Linux 8

Step 1: Verify Current Oracle Linux Version

Before starting the migration, validate the current OS version.

Command:

cat /etc/os-release

Example output:

NAME=”Oracle Linux Server”
VERSION=”7.x”

Step 2: Verify Current Repositories

Repository consistency is critical before performing upgrades.

Check repositories:

yum repolist

Why Repository Validation Matters

Broken or duplicate repositories may cause:

✔ Dependency failures
✔ Package mapping errors
✔ Upgrade inhibitors
✔ Incomplete migrations
✔ Boot failures

Add Leapp Repository Configuration

Navigate to repository directory:

cd /etc/yum.repos.d/

Create or validate Oracle Linux repositories.

Example:

[ol7_leapp]
name=Oracle Linux 7 Leapp Repository
baseurl=https://yum.oracle.com/repo/OracleLinux/OL7/leapp/x86_64/
enabled=1
gpgcheck=1

Refresh Repository Metadata
yum clean all
yum makecache

Step 4: Install Leapp Utility

Install required Leapp packages.

Command:

yum install -y leapp-upgrade leapp-data-oraclelinux

What Gets Installed?

Leapp installs:

✔ Upgrade actors
✔ Dependency analysis modules
✔ Migration logic
✔ Repository mapping data
✔ Upgrade workflows

Verify Leapp Installation

rpm -qa | grep leapp

Step 5: Understanding Leapp Preupgrade

Before performing the actual upgrade, Leapp performs extensive system analysis.

Command:
leapp preupgrade

What Happens During Preupgrade?

Leapp analyzes:

✔ Installed packages
✔ Drivers
✔ Repository configuration
✔ Bootloader state
✔ Kernel compatibility
✔ Unsupported packages
✔ Dependency conflicts
✔ Security policies

Operational Insight
The preupgrade phase prevents unsafe migrations before system modification begins.

Understanding Inhibitors

One of the most important Leapp concepts is inhibitors.

What Are Inhibitors?

Inhibitors are conditions that stop the upgrade from continuing safely.

If inhibitors exist, Leapp blocks the upgrade process.

Why Inhibitors Exist

Inhibitors protect systems from unsafe migration scenarios.

Examples:

✔ Unsupported repositories
✔ Duplicate repository entries
✔ Deprecated packages
✔ Missing answer files
✔ Third-party RPM conflicts
✔ Unsupported drivers
✔ Incorrect boot configuration

Real Repository Inhibitor Example

Example error:

Repository ol8_baseos_latest is listed more than once in the configuration.

Why This Happens

Possible causes:

✔ Duplicate .repo files
✔ Custom repositories
✔ Third-party repositories
✔ Incorrect migration preparation

Fixing Duplicate Repositories

Check repository directory:

ls -l /etc/yum.repos.d/

Review duplicate repository definitions:

grep -r “ol8_baseos_latest” /etc/yum.repos.d/

Remove duplicate entries carefully.

Understanding Leapp Answer Files

This is one of the most important upgrade concepts.

What Are Answer Files?

During upgrades, Leapp may require administrator confirmation for specific migration decisions.

Leapp stores these prompts inside answer files.

Location:

/var/log/leapp/answerfile

Why Answer Files Matter

Leapp blocks upgrades until required questions are answered.

Example:

Missing required answers in the answer file.

View Required Answers

Command:

leapp answer –section remove_pam_pkcs11_module_check.confirm=True

What Does This Do?

This command confirms specific upgrade actions required by Leapp.

Operational Insight

Answer files help administrators explicitly approve risky or environment-specific migration decisions.

Understanding Repository Migration

Oracle Linux 7 and Oracle Linux 8 use different repository structures.

Oracle Linux 7 Repositories

✔ ol7_latest
✔ ol7_UEKR6
✔ Optional repositories

Oracle Linux 8 Repositories

✔ BaseOS
✔ AppStream
✔ UEK repositories

Repository Mapping Workflow

OL7 Repositories
│
Repository Mapping
│
▼
OL8 BaseOS + AppStream

Understanding AppStream Repositories

Oracle Linux 8 introduces AppStream modular repositories.

Unlike Oracle Linux 7:

Packages are grouped into modules and streams.

Examples:

✔ Python streams
✔ NodeJS streams
✔ Database modules

This increases flexibility but also migration complexity.

Third-Party Repository Challenges

Enterprise systems commonly use third-party repositories.

Examples:

✔ EPEL
✔ Monitoring agents
✔ Security tools
✔ Vendor repositories
✔ Backup software

Example EPEL Problem

Example error:

No package epel-release available
Why This Happens

Possible causes:

✔ Repository incompatibility
✔ Incorrect release version
✔ Unsupported packages
✔ Missing metadata

Step 6: Execute Upgrade

Once inhibitors are resolved, begin the upgrade.

Command: leapp upgrade

What Happens Internally?

Leapp performs:

✔ Upgrade initramfs creation
✔ Bootloader modification
✔ Package migration
✔ Repository transition
✔ Kernel migration
✔ Service migration
Understanding Upgrade Initramfs

This is one of the most advanced upgrade concepts.

What is Upgrade Initramfs?

Leapp temporarily boots into a dedicated upgrade initramfs environment outside the running Oracle Linux 7 userspace.

This isolated environment safely performs package replacement operations.

Upgrade Boot Workflow

Normal OL7 Boot
│
▼
Upgrade Initramfs
│
▼
Package Migration
│
▼
OL8 Boot

Why Upgrade Initramfs Is Required

Leapp performs the upgrade outside the running Oracle Linux 7 userspace to avoid active package conflicts and dependency corruption during migration.

The temporary upgrade initramfs environment provides an isolated userspace where package replacement, repository switching, kernel migration, and dependency updates can occur safely without interfering with the currently running operating system.

This isolation significantly reduces the risk of package inconsistency and upgrade instability during major platform transitions.

Step 7: Reboot the System

After upgrade preparation:

reboot

The system boots into the temporary upgrade environment.

Migration occurs automatically.

Step 8: Verify Oracle Linux 8 Upgrade

After reboot completes:

Verify OS Version
cat /etc/os-release

Common Real-World Upgrade Problems

Many upgrade failures occur because of environment inconsistencies rather than Leapp itself.

Common Operational Problems

✔ Repository duplication
✔ Dependency conflicts
✔ Unsupported packages
✔ Missing drivers
✔ Bootloader issues
✔ EPEL incompatibility
✔ Service startup failures
✔ Network configuration mismatches
✔ SELinux conflicts

Upgrade Failure Recovery

If the upgrade process fails during migration, administrators should analyze Leapp reports, validate repositories, review inhibitors, and restore systems using backups or boot volume snapshots when necessary.

/var/log/leapp/leapp-report.txt

/var/log/leapp/leapp-upgrade.log

/var/log/leapp/leapp-preupgrade.log

Leapp logs provide detailed visibility into dependency analysis, migration stages, repository transitions, and package failures.

Rollback and Recovery Planning

Enterprise upgrades should always include rollback preparation.

Before upgrades:

✔ Create backups
✔ Snapshot boot volumes
✔ Validate rollback workflows
✔ Test upgrades in staging
✔ Document recovery procedures

Why Rollback Planning Matters

If upgrades fail unexpectedly:

Rollback capability reduces downtime and operational risk.
Enterprise Upgrade Best Practices

✔ Validate backups before upgrade
✔ Remove unsupported repositories
✔ Review Leapp reports carefully
✔ Resolve inhibitors completely
✔ Test upgrades in staging first
✔ Validate applications after migration
✔ Monitor services post-upgrade
✔ Maintain rollback procedures

Conclusion

Leapp provides a powerful and automated framework for migrating Oracle Linux 7 systems to Oracle Linux 8 through dependency analysis, repository validation, package migration, and upgrade orchestration.

Although the migration process is heavily automated, successful enterprise upgrades still require careful planning, repository consistency, inhibitor analysis, answer file validation, operational testing, and rollback preparation to ensure production stability.

Modern Linux upgrades are no longer simple package updates — they are enterprise platform modernization workflows requiring operational engineering discipline.

Responsive Web Design

Beginning:

Responsive web design is a web design or CSS style that makes a website look good on any screen size.

Note: The most popular responsive design technique is “Mobile First Design”.

To make the page optimized for mobile devices, first use this meta tag in the “head”
Сode:
<meta name="viewport" content="width=device-width, initial-scale=1" />

CSS Responsive Design can be achieved by using:

  • CSS units as percentages “%” (100%), “1fr” (for grid). “vw”.
  • “media query”.
  • CSS property: “max-width”, “min-width”.
  • Maybe something else.

A few of my own examples of what I use:

  1. If the width of the screen is 500 pixels or less, we do something, or if it is more than 500 pixels, we do something else.

@media(max-width: 500px){
p {
color: red;
}
}

@media(min-width: 500px){
p {
color: blue;
}
}

  1. This CSS code prevents images from overflowing and sets the automatic size for the picture:

img {
max-width:100%;
height: auto;
display: inline-block;
}

  1. My responsive content or page “wrapper” class. This wrapper in the example makes the content no wider than 560px and a width of 100% makes it adaptive to any screen:

.wrapper {
width: 100%;
max-width: 560px;
margin: 0 auto;
}

  1. Hide 1 and show 2 something depending on the screen size or dynamic style change. Hide the desktop version and show the mobile version of something instead. (A drop down menu or something else):

@media(width >= 500px){
p { color: red; }
}

@media(width <= 500px){
p { color: green; }
}

or

@media(width >= 500px){
.mobileVersion { display: none; }
.desktopVersion { display: block; color: red }
}

@media(width <= 500px){
.mobileVersion { display: block; color: green; }
.desktopVersion { display: none; }
}

  1. Personal experience and advice or technique:

I write CSS styles simultaneously for any screen, my method is the method of overriding the style above with the style below (hierarchy in CSS), for example, there is a regular style for large screens:

.testClass { color: red; }

Then under it I add the same CSS class and properties from the style above with values for other screens using CSS Media Queries:

.testClass { color: red; }

@media (max-width: 500px) {
.testClass { color: orange; }
}

@media (max-width: 300px) {
.testClass { color: blue; }
}

@media (max-width: 100px) {
.testClass { color: yellow; }
}

so I override the style above and create a new style with overriding method for screens that are not larger than 500 pixels, then for 200px with overwriting , then for 100px etc

  1. Note: Testing and debugging:
  • Resize the browser window to see the style changes.
  • Launch the developer tool in the browser through the menu and “Inspect” page and change the size of the window there.
  • In developer tools, you can choose mobile phone simulation: “Responsive Design Mode” (phone icon).
  • Of course, a test on a physical device.

Source and Learning:

Doc:

Presentation: AI Native Engineering

Ian Thomas shares a case study on embracing AI-native engineering within Meta’s Reality Labs. He explains the “Assess and Grow” framework, a maturity model designed to move teams from manual toil to AI-integrated innovation. He discusses real-world wins – including hitting 90% code coverage in record time – while addressing senior concerns like “code slop,” review fatigue, and maintaining quality.

By Ian Thomas

How We Picked Between PayPal and Crypto for a $500K/Year Digital Product Launch

The Problem We Were Actually Solving

In 2024 we launched a $99 CLI toolkit for engineers who automate cloud security posture. By month six we were processing $1,200 a day in PayPal fees and $300 a day in frozen funds awaiting manual review. Our chargeback rate was 0.4 %, well below the industry threshold, yet PayPal still hit us with a $2,500 rolling reserve. We had already moved our hosting and DNS to alternative providers to avoid central gatekeepers; payments were the last choke point.

What We Tried First (And Why It Failed)

We started with PayPal Payouts to pay affiliate commissions. The API latency was 4 seconds per payout, and every Sunday at 03:17 UTC the endpoint returned EMAIL_NOT_VERIFIED for 30 % of our 200 affiliates. Their support told us to log a ticket and wait three days; we missed our weekly payout window and lost three top affiliates.

Then we tried Stripe. Same problem: Stripe Radar flagged every European sale because the cardholder name contained an umlaut. Our false-positive rate reached 11 %, and Stripes phone support only works on weekdays. We were tired of begging for human escalation tickets.

The Architecture Decision

In April 2025 we built a dual-rail system: PayPal for US credit cards (still had to keep it for 60 % of our buyers) and Monero for the rest. Moneros 2.5-minute block time and 0.0004 XMR fee looked perfect on paper, but our first production batch revealed a flaw: users had to manually paste a payment URI into their wallet, which dropped conversion by 37 %.

We switched to a zero-knowledge checkout: the user clicks Purchase, the backend generates a session-specific Monero address with a 15-minute expiry, and the frontend polls our Node.js websocket endpoint for confirmation. The polling interval is 10 seconds, which gives us 90 % certainty within 2 minutes and still keeps the load under 120 requests per order.

The tradeoff was that we had to implement our own rate-limited memo decoder to prevent address-reuse spam. We chose the monero-cpp library over the Go wrapper because the C++ library exposed the transaction key scanning functions we needed; the Go version hid them behind a high-level struct that forced us to parse hex strings manually.

What The Numbers Said After

After six weeks on the dual system:

  • PayPal chargeback rate stayed at 0.4 %, but frozen funds dropped to $300 total (a 91 % reduction).
  • Monero orders grew from 12 % to 34 % of revenue.
  • Our biggest affiliate, who was based in Venezuela, started earning commissions again after PayPal banned him; he now refers 8 % of our traffic.
  • infra cost per $100 processed: PayPal $4.70, Monero $0.42.

The most painful lesson was chargeback handling. With PayPal we could dispute and win 78 % of cases automatically. With Monero there is no dispute layer; we simply mark a wallet as fraudulent and refund later. To compensate, we introduced a 30-second interactive CAPTCHA before checkout and reduced fraudulent orders from 1.8 % to 0.3 %.

What I Would Do Differently

I would not have waited until we hit $500K annual revenue to diversify. The moment PayPal placed our account under review for a single $2 transaction, we should have immediately spun up the zero-knowledge Monero flow instead of treating it as an experimental branch.

I would also have standardised on a single crypto library from day one. We wasted two engineer-weeks rewriting memo parsers after the Go wrapper changed its ABI between patch releases. Settling on monero-cpp and pinning the SHA to a4b1c3d would have saved us that pain.

Finally, I would have opened a small USD Coin (USDC) stream in parallel for US buyers who refuse Monero. It adds a third payment button but keeps our PayPal dependency to only 45 % of revenue, which is low enough to survive another arbitrary freeze.

Post-Mortem: Why My Blog Cover Images Silently Failed to Restore from S3

Six blog posts on my portfolio had broken cover images for longer than I’d like to admit. The images were in S3. The management command to restore them had been written and run. And yet — nothing. Blank covers, every time.

Here’s the full breakdown of what went wrong, why, and how it got fixed.

The Setup

My portfolio backend is a Cookiecutter Django project running on a DigitalOcean droplet with Docker Compose. Blog post cover images are stored on AWS S3. The cover field on the BlogPost model is an ImageField that stores a relative path like blog_posts/deploy.webp — Django’s S3 storage backend handles prepending the media/ prefix and building the full URL.

When I migrated away from using Hashnode as a headless CMS and imported all posts into my own Django backend, the cover images came along as CUID-based filenames (e.g. blog_posts/cmoxrumae00ms2em7bje5at07.png). Those CUIDs don’t exist in S3 — the actual files were uploaded separately with descriptive names like deploy.webp, manual.webp, sortinghashnode.webp.

To fix this, Gemini wrote restore_covers.py, a management command with two matching strategies:

  1. Exact CUID match — looks for blog_posts/{cuid}.webp etc. in S3
  2. Fuzzy slug match — tokenizes the post slug and looks for S3 filenames with overlapping keywords

The command was run. Six posts still had broken covers.

Root Cause 1: The Fuzzy Matcher Couldn’t Tokenize Concatenated Filenames

The S3 filenames are lowercase concatenated words: sortinghashnode.webp, trackingpage.webp, postmortem.webp. The fuzzy matcher works by calling get_keywords() on both the post slug and each S3 filename, then computing the set intersection.

Here’s the problem. get_keywords() uses re.findall(r"[a-zA-Z0-9]+", text) to tokenize. Applied to a filename:

get_keywords("sortinghashnode.webp")
# → {"sortinghashnode"}   ← one token

And for the post slug:

get_keywords("sorting-hashnode-series-posts-how-to-display-the-latest-post-first")
# → {"sorting", "hashnode", "series", "posts", "display", "latest", "first"}

The intersection of {"sortinghashnode"} and {"sorting", "hashnode", ...} is empty. Score = 0. No match.

The same failure applied to every concatenated filename in the bucket. trackingpage.webp couldn’t match a slug containing tracking and page. postmortem.webp couldn’t match a slug containing mortem (since post is a stop word). None of them scored above 0.

The fix was to replace the plain set intersection with substring containment, enforcing a minimum token length of 4 characters to prevent false positives from short words:

min_token_len = 4
overlap = set()
for pk in post_keywords:
    for fk in file_keywords:
        if pk == fk or (
            len(pk) >= min_token_len
            and len(fk) >= min_token_len
            and (pk in fk or fk in pk)
        ):
            overlap.add(pk)

Now "sorting" in "sortinghashnode" → True, score += 1. "hashnode" in "sortinghashnode" → True, score += 1. The correct file gets matched.

Root Cause 2: No AWS Credentials in the Local Environment

The second reason the command silently failed: it was run via docker compose -f docker-compose.local.yml, which loads .envs/.local/.django. That file has no AWS credentials.

When storage.listdir("blog_posts") is called with no S3 credentials, it either errors out silently (caught by a bare except Exception) or returns an empty list because the local filesystem storage backend is active instead of S3. The command’s output showed:

Could not list storage directory directly: ...
Fuzzy matching won't be available.

But the overall command still exited 0 with a summary that made it look like it ran fine. With zero files in storage_files, the fuzzy loop had nothing to iterate over — so every post hit the “no existing file found in storage” branch.

The management command is designed to run in the production container, where .envs/.production/.django already has the correct AWS credentials wired up.

Root Cause 3: One Image Was Never Uploaded to S3

Even with both fixes above, the post “How I Fixed the Hashnode GraphQL API Stale Cache Bug (Stellate CDN)” would still have a broken cover — because no matching file exists in S3 at all. The DB had blog_posts/cmlyqj0cc006627lvguola3gg.png and no file with a descriptive name was ever uploaded for it.

This one requires a manual upload via Django admin.

The Fix

For the five posts with known S3 matches, I wrote a Django data migration that directly sets the correct cover paths:

COVER_FIXES = {
    "how-to-manually-backup-wordpress-sites-via-ssh": "blog_posts/manual.webp",
    "deploying-cookiecutter-django-on-a-digitalocean-droplet-ubuntu-24-04-lts": "blog_posts/deploy.webp",
    "post-mortem-the-march-2026-axios-supply-chain-attack": "blog_posts/postmortem.webp",
    "sorting-hashnode-series-posts-how-to-display-the-latest-post-first": "blog_posts/sortinghashnode.webp",
    "tracking-page-views-in-a-react-spa-with-google-analytics-4": "blog_posts/trackingpage.webp",
}

def fix_covers(apps, schema_editor):
    BlogPost = apps.get_model("blogs", "BlogPost")
    for slug, cover_path in COVER_FIXES.items():
        BlogPost.objects.filter(slug=slug).update(cover=cover_path)

This runs automatically on python manage.py migrate during the next production deploy — no manual SSH step needed.

For the fuzzy matcher, the substring containment fix was patched into restore_covers.py so future runs work correctly for any similarly named files.

For the sixth post, a manual image upload to Django admin is the remaining action item.

What I’d Do Differently

The real issue is that the restore command’s failure mode was too quiet. It logged “fuzzy matching won’t be available” but still printed a clean summary with zeroes in the “could not restore” column for cases where the file list was empty. That made it look successful.

A better design: if storage.listdir() fails entirely, the command should exit early with a non-zero code rather than continuing with no files to match against. Silently succeeding at nothing is worse than loudly failing at something.

The slug-to-filename mismatch was also a predictable problem from the start. The files were uploaded manually with short descriptive names, but the DB records came from Hashnode with long CUIDs. A mapping file (even a simple JSON dict of slug → filename) would have made the restore command deterministic instead of relying on fuzzy heuristics.

Meet Chip: The Circuit.Rocks Design Academy Mascot

If you’ve spent any time on our store, our socials, or the workbench at a Circuit.Rocks workshop lately, you’ve probably spotted a small green CRT-headed robot peeking out from behind a breadboard. That’s Chip — the official mascot of Circuit.Rocks Design Academy.

Who is Chip?

Chip is a tiny vintage-computer-style robot with a glowing lightbulb antenna — built equal parts curiosity and 5V logic. He lives on a maker’s workbench surrounded by oscilloscopes, breadboards, soldering irons, and the occasional half-finished project. Chip’s job is to make electronics feel approachable: not intimidating, not gatekept, just a fun thing you can poke at, break, and learn from.

Whenever you see Chip on a guide, a tutorial reel, a worksheet, or a workshop poster — it’s a signal that this is content built specifically to help beginners (and tinkerers refreshing their fundamentals) actually understand what’s going on under the hood, not just copy-paste a wiring diagram.

Why a mascot?

Electronics has a reputation problem. Datasheets are dense. Forum answers assume you already know the lingo. The first time you smell a fried capacitor, you wonder if you’re cut out for this at all.

Chip exists to flip that script. The notebook on his desk says “Bug in the code?” not because debugging is shameful, but because every maker fights bugs — Chip just happens to keep a cheerful expression while doing it. The mascot is a small, deliberate reminder that learning to build things is supposed to be messy, weird, and fun.

Where you’ll see Chip

Chip is the face of our growing Design Academy library — short-form tutorial videos, downloadable beginner guides, project walk-throughs, and component primers. If you’re new to electronics and want a starting place that doesn’t assume two semesters of circuit theory, the Chip page is where to begin.

You’ll also catch him on our Circuitrocks social channels, popping up next to product launches, workshop announcements, and the occasional “this resistor exploded, here’s why” cautionary tale.

Say hi

Have an idea for a Chip episode? A topic you wish someone would just explain from scratch? Drop us a message — and if you spot Chip merch in the wild, send a photo. He’s photogenic and he knows it.

Visit circuit.rocks/pages/chip to meet him properly.

Originally published on blog.circuit.rocks.