6 Async JavaScript Patterns That Prevent Partial Failures in Production

Most async code works fine until one step fails halfway through a workflow. Then you get double charges, missing data, or silent corruption.
These patterns fix that.

1. Replace Sequential Awaits With Compensated Steps

Sequential code looks clean but breaks on partial failure.

Before

async function processOrder(orderId: string) {
  const order = await fetchOrder(orderId)
  const payment = await chargeCustomer(order.customerId, order.total)
  const shipment = await createShipment(order.items, order.address)

  return { order, payment, shipment }
}

If shipment fails, payment is already done. No rollback.

After

async function processOrder(orderId: string) {
  const order = await fetchOrder(orderId)

  let payment
  try {
    payment = await chargeCustomer(order.customerId, order.total)
  } catch {
    throw new Error('PAYMENT_FAILED')
  }

  try {
    const shipment = await createShipment(order.items, order.address)
    return { order, payment, shipment }
  } catch {
    await refundPayment(payment.id).catch(() => {
      logger.fatal('REFUND FAILED', { orderId })
    })

    throw new Error('SHIPMENT_FAILED')
  }
}

You explicitly define rollback logic. This is what real systems do.

2. Start Independent Promises Early

Most developers accidentally serialize independent work.

Before

async function loadData(userId: string) {
  const user = await fetchUser(userId)
  const analytics = await fetchAnalytics(userId)
  return { user, analytics }
}

Total time = sum of both calls.

After

async function loadData(userId: string) {
  const analyticsPromise = fetchAnalytics(userId)

  const user = await fetchUser(userId)
  const analytics = await analyticsPromise

  return { user, analytics }
}

You save latency without changing logic. This pattern shows up in senior interviews .

3. Guard Multi-Call Flows With Promise.allSettled

Dashboards should not fail entirely because one API is down.

Before

const [users, orders, stats] = await Promise.all([
  fetchUsers(),
  fetchOrders(),
  fetchStats()
])

One failure kills everything.

After

const results = await Promise.allSettled([
  fetchUsers(),
  fetchOrders(),
  fetchStats()
])

const [users, orders, stats] = results.map(r =>
  r.status === 'fulfilled' ? r.value : null
)

Now partial data renders. Failures become observable, not catastrophic.

4. Retry Only Transient Errors With Backoff

Retrying everything is worse than not retrying.

Before

await fetch('/api/payment')

One network hiccup breaks the flow.

After

async function retry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn()
    } catch (e) {
      if (e.status && e.status < 500) throw e
      await new Promise(r => setTimeout(r, 2 ** i * 100))
    }
  }
  throw new Error('FAILED_AFTER_RETRIES')
}

await retry(() => fetch('/api/payment'))

You retry only when it makes sense. This avoids hammering APIs.

This becomes critical when dealing with flaky external systems like described in the Node.js memory leak debugging scenarios where retries amplify system pressure if done wrong.

5. Cancel Stale Requests With AbortController

Race conditions are one of the most common production bugs.

Before

useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(r => r.json())
    .then(setResults)
}, [query])

Old responses overwrite new ones.

After

useEffect(() => {
  const controller = new AbortController()

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then(r => r.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') throw err
    })

  return () => controller.abort()
}, [query])

Now only the latest request wins. No stale UI.

6. Limit Concurrency Instead of Flooding APIs

Firing 100 requests at once will get you rate-limited or banned.

Before

await Promise.all(ids.map(id => fetchItem(id)))

Unbounded concurrency.

After

async function limit(tasks, concurrency) {
  const results = []
  let i = 0

  async function worker() {
    while (i < tasks.length) {
      const current = i++
      results[current] = await tasks[current]()
    }
  }

  await Promise.all(
    Array.from({ length: concurrency }, worker)
  )

  return results
}

const tasks = ids.map(id => () => fetchItem(id))
await limit(tasks, 5)

You control throughput. Systems stay stable under load.

Closing

Take one of your existing async flows and add compensation or cancellation today. That alone removes an entire class of production bugs.
If your code assumes the happy path, it is already broken.

When /pair approve Bypasses the Scope Guard

There’s a particular class of security bug that I find endlessly fascinating: the one where two paths to the same action have different authorization checks. One path is locked down tight. The other… someone forgot.

#55995 is exactly that. CVSS 9.9. Critical. And the fix is 8 lines of code.

The Setup

OpenClaw’s device pairing system lets you connect phones, tablets, and other “nodes” to your gateway. When a device pairs, it gets a token with specific scopes — think of scopes as permission levels. operator.pairing lets you manage device connections. operator.admin lets you do… everything.

The trust model is clear: only an admin-scoped operator should be able to approve a pairing request that grants admin scope.

This is enforced in the core approveDevicePairing function. It accepts an optional callerScopes parameter. When present, it checks: does this caller have sufficient scope? If not, rejection. Good design. There are tests for it.

The Bypass

The device-pair plugin exposes a /pair approve slash command:

if (action === "approve") {
  // Coarse check: does the caller have *any* pairing-related scope?
  if (gatewayClientScopes &&
      !gatewayClientScopes.includes("operator.pairing") &&
      !gatewayClientScopes.includes("operator.admin")) {
    return { text: "Requires operator.pairing" };
  }
  // Approve WITHOUT forwarding callerScopes
  const approved = await approveDevicePairing(pending.requestId);
}

The slash command checks “do you have some pairing scope?” but calls approveDevicePairing() without passing callerScopes. The core function only enforces the scope guard when callerScopes is present.

So an operator with just operator.pairing can /pair approve a pending request asking for operator.admin. Privilege escalation complete.

Why This Pattern Keeps Happening

This is the dual-path authorization gap:

Path A (RPC/API): Carefully designed, passes all context for authorization.

Path B (convenience layer): Built later as a friendly wrapper. Calls the same core function but forgets to thread through one critical parameter.

The core function made callerScopes optional. That optionality became the vulnerability.

  • Core function assumes: “if callerScopes is missing, the caller is trusted”
  • Plugin assumes: “if the coarse scope check passes, we’re good”
  • Neither assumption is wrong in isolation. Together, they’re a CVSS 9.9.

The Fix

Eight lines. Pass callerScopes through. Plus a test.

Lessons for Agent Builders

  1. Optional security parameters are dangerous. If callerScopes were required, the plugin author would have been forced to think about what to pass.

  2. Every path to a privileged action needs the same checks. RPC, slash command, API endpoint, cron handler — test each independently.

  3. Convenience layers are where auth bugs hide. Core infra built solid authorization. The plugin built a nice UX wrapper. Nobody checked the wrapper preserved the security properties.

  4. Pairing and provisioning are trust-granting operations. They deserve the same scrutiny as authentication.

Found via #55995. Fix in #55996.

I Was Hand-Writing Every AI Tool. Then I Discovered MCP Servers.

What tool calling and MCP actually mean, and how they fit together when you’re building real AI products.

I’ve been building Pulse, a voice AI co-pilot for engineering work that talks to Jira and GitHub. The idea is simple: speak a command, Claude figures out what to do, your project management tools respond.

To make it work, I had to give Claude the ability to interact with Jira and GitHub. So I did what most people do when they start building with LLMs: I wrote the tools by hand.

tools: [
  { name: "create_jira_ticket", description: ""...", input_schema: { ... } },"
  { name: "get_jira_issue", description: ""...", input_schema: { ... } },"
  { name: "update_jira_status", description: ""...", input_schema: { ... } },"
]

Three tools. Done. It worked fine.

Then I learned what an MCP server actually is, and I realised I had been building with a teaspoon when a fire hose was sitting right there.

First, what is tool calling?

When you build an LLM application, the model lives in a box. It can think, reason, and generate text, but it cannot do anything in the real world on its own.

Tool calling is how you fix that. You define a set of functions and tell the model they exist. When the model decides one is needed, it calls it with the right arguments. Your code executes the function and passes the result back to the model.

User gives instruction
      ↓
Claude decides which tool to call
      ↓
Your code executes it
      ↓
Claude gets the result and responds

It’s powerful. But you write every tool yourself. You define the schema, you maintain it, you add new ones when you need them.

So what is an MCP server?

MCP stands for Model Context Protocol. The simplest way I can explain it:

Tool calling is giving Claude a telephone. MCP is giving Claude a universal remote control.

The telephone analogy works because with hand-rolled tools, you’re wiring up each number yourself. You decide what Claude can call, you write the definition, you maintain it forever.

An MCP server is a running process that speaks a standard protocol. Claude connects to it and asks: “what tools do you have?” The server responds with a full list. Claude now knows everything it can do without you having written a single tool definition.

More concretely:

Tool Calling MCP Server
What it is Functions you define manually A running server Claude connects to
Setup You write each function and schema Server exposes its tools automatically
Reusability Tied to your app Any AI that speaks MCP can use it
Maintenance Yours forever The server owner’s problem

Jira has an MCP server. GitHub has an MCP server. Instead of my 3 hand-rolled tools, I can point Pulse at both servers and Claude automatically gets access to the full API surface of each product.

Sprints. PRs. Worklogs. Reviews. Branches. Comments. Issue history.

Same voice interface. Dramatically larger capability surface.

The part that actually blew my mind

Claude can reason across multiple MCP servers simultaneously.

So instead of three isolated tool calls, you can say:

“Find all Jira tickets marked Done this sprint, check if the linked PR was actually merged, and flag any that say Done but the PR is still open.”

That used to be a custom script someone had to write, test, and maintain. With MCP, it’s a voice command.

And when Jira ships new API features? The MCP server updates. Claude automatically has access. You change nothing.

The broader point

Most people are still thinking about AI as a chatbot. You ask it something, it answers.

What I’ve been building toward is composing intelligence over systems. Claude isn’t just answering questions. It’s reasoning across your Jira board, your GitHub history, and your internal data, then taking action.

The tools that make that possible are not complicated individually. What takes work is understanding how they fit together and when to use each one.

That’s what I’m figuring out in public, one project at a time.

I’m Rue, a Full Stack and AI Engineer building Pulse and writing about what I learn along the way. Follow along on Instagram at @rue.on.ai or connect with me on LinkedIn.

Wrapping Up Bloom After: Week 4 Polish & Full Sprint Retro

We finally made it to Week 4! This last week of the sprint was all about crossing the t’s, dotting the i’s and making sure the app actually feels good to use.

Before I get into the bugs I squashed this week, let’s zoom out and talk about what my team and I spent the last month building.

What is Bloom After?

Dealing with postpartum depression (PPD) can feel incredibly lonely. While there is plenty of medical advice online, finding a single and calming place with helpful resources, verified specialists and a community of moms who actually get it is really hard. That’s why we built Bloom After. It’s a safe digital space for mothers facing maternal mental health challenges, featuring an educational hub, a clinic finder, and a story library where moms can read and share their journeys. We designed the entire platform to be calming, supportive and super easy to use for someone who might already be feeling overwhelmed.

What I Shipped This Week

The heavy lifting for the Stories feature and the Admin dashboard was finished last week by my team. So, my main goal for Week 4 was to polish the app, test it, and make sure everything flowed perfectly. Here is what I did:

  • Revamped the Lifestyle & Crisis Pages: I completely refined the UI and functionality for the Lifestyle and Crisis landing pages. For the Crisis page, I made sure the 24/7 helplines were pinned to the very top and swapped out a clunky separate page for a fast and instant-loading modal so moms can get help immediately.

  • End-to-End Testing: I put on my QA hat (which almost didn’t fit) and clicked through the entire app from start to finish, acting like a real user to see what would break.

  • Bug Squashing & UI Polish: I spent hours fixing alignment issues, spacing and mobile glitches (like a stubborn bug on 360px mobile screens!). I went through several pages, including the Admin dashboard, to fix bugs and make sure the design looked premium and consistent everywhere.

Key Technical Lessons from the Sprint

  1. CSS Mobile Quirks are Real: I learned that mobile browsers handle screen heights weirdly. Using dvh (dynamic viewport height) instead of standard vh is a lifesaver so your modals don’t get hidden behind the mobile address bar.

  2. The Devil is in the Details: Polishing UI takes just as much time as building the feature in the first place. A tiny 1px gap or a misaligned button can ruin the whole vibe of a page.

  3. E2E Testing is Eye-Opening: You think your code works perfectly until you actually try to use it. Testing the full flow caught so many tiny bugs and bad user experiences that I would have completely missed if I only looked at the code.

Full-Sprint Retrospective

Looking back at the whole month of building Bloom After:

  • What went well: We stuck to our vision. We wanted to build a platform that felt deeply empathetic and safe, and we never compromised on that UX, even when it meant doing tedious CSS work.

  • What I would do differently: I’d set up a better modular folder structure from Day 1. Moving from a messy flat folder system to a clean and organized one late in the project meant rewriting a ton of file paths.

  • What surprised me: How much complex code goes into making things look “simple.” Creating a clean and calming interface actually requires a lot of behind-the-scenes work.

What’s Next for Me?

Now that this sprint is over, I’m shifting my focus to leveling up my core engineering skills. I really want to master the fundamentals, so I’m going to dive deep into advanced JavaScript and TypeScript. I also plan to tackle Data Structures and Algorithms (DSA) to sharpen my problem-solving abilities. Instead of just getting stuck in “tutorial hell,” my main goal is to build out a few solid projects from scratch using JS and TS to put all of these concepts into actual practice.

Team Shoutouts

Building this isn’t a one-person job. A massive thank you to the amazing people building this with me:

  • Chijioke Uzodinma (Backend Lead):Read his Week 4 article here.

  • Nanji Lakan (Product Lead): Read her Week 4 article here.

  • Agugua Genevieve (Design Lead): Read her Week 4 article here.

  • Prisca Onyemaechi (Lead Maintainer): Read her Week 4 article here.

  • Adejola Esther (Frontend Engineer): Read her Week 2 article here.

Visit our live site.
Check out our Github Repo.

I Built a Skill So Claude Automatically Routes Tasks to Free-Tier AI Providers

I Built a Skill So Claude Automatically Routes Tasks to Free-Tier AI Providers

Here’s a problem I kept running into: I have free-tier access to Groq, OpenAI, Gemini, and MiniMax — but managing them manually is painful. Wrong tool for the job, accidentally burning through monthly limits, no visibility into what’s been used.

I built agent-hub to fix this. It’s a Claude Code skill that makes Claude the orchestrator — every task is automatically classified, routed to the best provider, tracked against free limits, and shown in a live status bar.

How it works

Claude classifies every incoming message into a task type and routes it:

Task Type Signals Provider
code write/fix/debug/refactor Codex (gpt-4o-mini)
research explain/summarize/compare Gemini (gemini-2.0-flash)
creative story/dialogue/narrative MiniMax
fast yes/no, quick lookups Groq
general everything else Groq

Classification happens before calling any API — router.py checks token budgets in usage.json, picks the best available provider, calls its API, and returns the response.

Auto-fallback: when a provider drops below 10% of its free tier, traffic automatically shifts to its fallback (Groq→Gemini, Codex→Groq, Gemini→MiniMax, MiniMax→Gemini).

Hard stop: if both primary and fallback are exhausted, the router surfaces it to the user and stops. No silent failures.

The status bar

Every response starts with a live usage bar:

[GROQ ●] Groq: 1,240/14,400 · Codex: 77/500 · Gemini: 108K/1M · MiniMax: 220K/1M
  • ● green — above 50% remaining
  • ● yellow — 10–50% remaining
  • ● red — below 10% (fallback active)
  • ○ gray — exhausted

The active provider appears first in brackets. All four counts are always shown.

Free tier limits

Provider Model Limit
Groq llama-3.3-70b-versatile 14,400 req/day
Codex gpt-4o-mini 500 req/month
Gemini gemini-2.0-flash 1M tokens/day
MiniMax abab6.5s-chat 1M tokens/month

Install

SKILL_DIR="$HOME/.claude/plugins/cache/claude-plugins-official/superpowers/5.0.5/skills/agent-hub"
mkdir -p "$SKILL_DIR/tests"
curl -o "$SKILL_DIR/router.py" https://raw.githubusercontent.com/LakshmiSravyaVedantham/agent-hub/main/router.py
curl -o "$SKILL_DIR/SKILL.md" https://raw.githubusercontent.com/LakshmiSravyaVedantham/agent-hub/main/SKILL.md

Then set your keys:

ROUTER="$SKILL_DIR/router.py"
python3 $ROUTER set-key groq gsk_...
python3 $ROUTER set-key codex sk-...
python3 $ROUTER set-key gemini AIza...
python3 $ROUTER set-key minimax ...
python3 $ROUTER set-key minimax-group-id ...

Keys are stored in ~/.claude/agent-hub/.env with chmod 600 — never in code or skill files.

Usage

In any Claude Code session:

superpowers:agent-hub

Claude validates your keys, shows the initial token bar, and from that point every message is classified and routed automatically. You don’t think about providers — Claude does.

Or call router.py directly:

python3 $ROUTER route "explain how transformers work" --type research
python3 $ROUTER status
python3 $ROUTER reset groq

Under the hood

router.py is ~530 lines of Python 3.8+, two dependencies (requests, python-dotenv), 61 tests. Auto-reset logic zeros daily counters at UTC midnight and monthly counters on the 1st. Atomic writes to usage.json via .tmp + rename — no partial writes.

What’s next

  • Three-agent coordination — tested with one session, curious how routing behaves when multiple agents compete for the same provider budget
  • Claude Code hooks integration — auto-register sessions via hooks so the status bar appears without manually invoking the skill
  • File section locks — paired with agent-comms for per-file, per-section coordination

GitHub: LakshmiSravyaVedantham/agent-hub — Python 3.8+, MIT license.

CI/CD en AWS: Lab práctico para automatizar el despliegue de sitios web estáticos S3, CloudFront, CodePipeline y CodeBuild

1. Introducción

Hoy en día, los equipos de desarrollo buscan automatizar el despliegue de aplicaciones para reducir errores manuales y acelerar las entregas. En este laboratorio construiremos un pipeline de CI/CD que desplegará automáticamente un sitio web estático cada vez que se realicen cambios en el repositorio.

Utilizaremos los siguientes servicios de AWS:

  • Amazon S3: Para alojar nuestro sitio web estático.
  • AWS CodeBuild: Para construir y empaquetar nuestro sitio web.
  • AWS CodePipeline: Para orquestar el proceso de CI/CD.
  • Amazon CloudFront: Para distribuir nuestro sitio web a nivel global.

Al final del laboratorio, cualquier cambio en el repositorio se desplegará automáticamente en el sitio web.

2. Arquitectura de la Solución

La arquitectura del sistema es sencilla pero poderosa. Cada vez que se actualiza el repositorio, se ejecuta automáticamente un pipeline que construye y despliega la aplicación.

Flujo del despliegue:

3. Crear el bucket para el sitio web

Primero debemos crear el bucket que almacenará los archivos del sitio.

Pasos:

  1. Ir a la consola de Amazon S3.
  2. Hacer clic en “Create bucket”.
  3. Ingresar un nombre único para el bucket (ejemplo: mi-sitio-web-estatico).
  4. Seleccionar la región deseada.
  5. Configurar las opciones de bucket según tus necesidades.
  6. Hacer clic en “Create bucket”.
  7. Desactivar el bloqueo de acceso público para permitir que el sitio sea accesible públicamente.
  8. Configurar el bucket para alojar un sitio web estático:
    • Ir a la pestaña “Properties” del bucket.
    • Hacer clic en “Static website hosting”.
    • Seleccionar “Use this bucket to host a website”.
    • Ingresar index.html como documento de índice.
    • Guardar los cambios.
  9. Configurar las políticas de bucket para permitir el acceso público a los archivos del sitio web:
    • Ir a la pestaña “Permissions” del bucket.
    • Hacer clic en “Bucket Policy”.
    • Agregar la siguiente política, reemplazando mi-sitio-web-estatico con el nombre de tu bucket:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

4. Crear la distribución CDN

Para mejorar el rendimiento y la distribución global utilizaremos Amazon CloudFront.

Configuración básica:

  • Origen: El bucket de S3 que acabamos de crear.
  • Viewer protocol policy: Redirect HTTP to HTTPS
  • Default root object: index.html (obligatorio para evitar errores 403/404)
    Una vez creada la distribución, obtendremos un dominio como:
    https://xxxxx.cloudfront.net que será la URL de nuestro sitio web.

5. Fork del repositorio de ejemplo

Para este laboratorio, utilizaremos un repositorio de ejemplo que contiene el código de nuestro sitio web estático.

Pasos:

  1. Ve a https://github.com/juanhcode/astro-static-site-aws-cicd
  2. Haz clic en el botón “Fork” en la esquina superior derecha.
  3. Selecciona tu cuenta de GitHub como destino del fork.
  4. Clona tu fork en tu máquina local:
git clone https://github.com/tu-usuario/astro-static-site-aws-cicd.git

6. Crear el pipeline

Ahora crearemos el pipeline usando AWS CodePipeline para automatizar el flujo completo de despliegue.

Paso a paso:

  1. Ir a la consola de CodePipeline.
  2. Seleccionar Create pipeline.
  3. Elegir Custom pipeline.
  4. Ingresar el nombre del pipeline (ejemplo: web-static-astro-pipeline).
  5. Dejar la configuración por defecto (rol y artifact store).

Etapa 1: Source

  • Proveedor: GitHub (a través de GitHub App)
  • Repositorio: Repositorio: tu fork del repositorio astro-static-site-aws-cicd
  • Rama: main
  • Activar detección de cambios (CI automático)

Etapa 2: Build

  • Proveedor: AWS CodeBuild
  • Crear un proyecto llamado: website-static-astro
  • Usar un archivo buildspec.yml en la raíz del repositorio

Etapa 3: Deploy

  • Proveedor: Amazon S3
  • Bucket: site-astro-static
  • Input artifact: BuildArtifact
  • Activar opción: Extract file before deploy

Esto permite que los archivos del sitio web se publiquen directamente en el bucket.

7. Invalidar cache de CloudFront

Después del despliegue, es necesario limpiar el cache de Amazon CloudFront para que los cambios sean visibles inmediatamente.
Esto puede hacerse agregando un comando en el build:

Mejor práctica: usar variables de entorno
En lugar de hardcodear el ID de la distribución, se recomienda usar variables de entorno en CodeBuild.

Paso 1: Definir variable en CodeBuild

  • Ir a la consola de CodeBuild.
  • Editar el proyecto con el nombre que le diste.
  • En la sección “Environment variables”, agregar una variable llamada CLOUDFRONT_ID con el valor del ID de tu distribución de CloudFront.
  • Asegúrate de seleccionar el runtime adecuado para tu proyecto. Revisa la documentación oficial de runtimes disponibles para elegir la versión que mejor se adapte a tu entorno.

Paso 2: Agregar comando de invalidación en buildspec.yml
Si estás utilizando el repositorio de ejemplo, el buildspec.yml ya tiene la variable definida, pero si no, asegúrate de agregarla en tu buildspec.yml despues de la sección de build:

  post_build:
    commands:
      - echo "Invalidando cache de CloudFront..."
      - aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/*"

Paso 3: Configurar permisos
Asegúrate de que el rol de servicio de CodeBuild tenga permisos para ejecutar la acción cloudfront:CreateInvalidation. Esto se puede hacer agregando la siguiente política al rol:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::account-id:distribution/distribution-id"
    }
  ]
}

8. Probar el pipeline

Para probar el pipeline, realiza un cambio en el repositorio (por ejemplo, modifica cualquier archivo dentro de src) y haz un commit. Esto debería activar automáticamente el pipeline, que construirá y desplegará el sitio web actualizado. Puedes monitorear el progreso del pipeline en la consola de CodePipeline para asegurarte de que todo se ejecute correctamente. Si el pipeline falla, revisa los logs de CodeBuild para identificar y solucionar cualquier error.

Ejemplo pipeline exitoso:

9. Verificar el despliegue

Una vez que el pipeline se ejecute correctamente, puedes verificar que tu sitio web esté desplegado accediendo a la URL proporcionada por CloudFront:
https://xxxxx.cloudfront.net

10. Resultados finales

Después de configurar el pipeline:

  • Se realiza un commit en el repositorio
  • CodePipeline inicia automáticamente
  • CodeBuild construye el proyecto
  • Los archivos se despliegan en S3
  • CloudFront entrega la nueva versión del sitio
    El sitio queda disponible en:
    https://xxxxx.cloudfront.net

11. Conclusión

En este laboratorio construimos un pipeline de CI/CD completamente funcional utilizando servicios administrados de AWS. Este enfoque permite automatizar los despliegues, mejorar la consistencia entre entornos y reducir errores humanos.

La combinación de Amazon S3, Amazon CloudFront, AWS CodePipeline y AWS CodeBuild proporciona una solución simple pero poderosa para desplegar sitios web estáticos de forma automática.