All projects META · CI/CD

This Portfolio

The site you're reading right now: a static, bilingual portfolio with no build step, deployed to AWS through a GitHub Actions pipeline that authenticates with a short-lived OIDC token instead of a long-lived access key.

GitHub ActionsAWS S3CloudFrontOIDC

THE SMALLEST PROJECT HERE, ON PURPOSE

No framework, no build step, no keys.

Day to day I work in Google Cloud. This site is deliberately on AWS instead — a small, low-stakes place to keep that muscle current. It's plain HTML, CSS, and JavaScript: no bundler, no package install, no framework. The bilingual EN/ES toggle is about 40 lines of vanilla JavaScript that swaps text from data-en/data-es attributes already sitting on each element.

The part worth showing isn't the HTML — it's the pipeline. A push to main syncs the site to S3 and invalidates CloudFront automatically, and the GitHub Actions workflow never holds an AWS access key: it assumes an IAM role for the duration of the job via OpenID Connect, so there's no long-lived secret sitting in a repository setting.

0long-lived AWS keys in the repo
3workflow steps: sync, invalidate, done
2languages, one static HTML file
Small scope,done properly.

DEPLOY PIPELINE

Push to main, live in under a minute.

GitHub Actions assumes a scoped IAM role via OIDC, syncs the website/ folder to S3, and invalidates the CloudFront cache so the change is visible immediately rather than waiting out a TTL.

This portfolio's deploy pipeline: a developer pushes to the GitHub main branch, which triggers a GitHub Actions workflow that assumes an IAM role via OIDC, syncs the website folder to S3, and invalidates CloudFront, which serves the visitor. SOURCE CI/CD AWS DELIVERY Developer git push GitHub — main source of truth GitHub Actions OIDC · no static keys on push to main IAM Role OIDC trust policy S3 bucket aws s3 sync --delete CloudFront CDN + cache invalidation Visitor HTTPS, any browser
Source / AWS resource
CI/CD workflow
End client

Three steps, one workflow file: checkout, aws s3 sync --delete ./website s3://<bucket>, aws cloudfront create-invalidation --paths '/*'.

WHAT EACH PIECE DOES

Four small, well-understood pieces.

01

Static site

Plain HTML/CSS/JS, no build step. A vanilla JS toggle swaps text from data-en/data-es attributes already on each element, including the page <title> and meta description.

HTMLCSSVanilla JS
02

GitHub Actions

One workflow, triggered on push to main: checkout, assume role, sync, invalidate. No secrets stored in the repository.

GitHub Actions
03

OIDC authentication

The workflow requests a short-lived token and exchanges it for temporary AWS credentials scoped to one IAM role — nothing to rotate, nothing to leak.

OIDCIAM
04

S3 + CloudFront

S3 holds the synced files; CloudFront fronts it with HTTPS and caching, invalidated on every deploy so changes are visible immediately.

S3CloudFront

WHY IT'S BUILT THIS WAY

A few deliberate trade-offs.

  • OIDC role assumption instead of static AWS access keys: nothing long-lived to store as a GitHub secret, nothing to rotate or accidentally leak in a log.
  • No framework or build step, on purpose — a portfolio site doesn't need one, and it keeps the deploy pipeline to three steps instead of a build matrix.
  • Bilingual text lives as data-en/data-es attributes directly on each element rather than in separate locale files — simpler to keep in sync for a page this size, at the cost of not scaling past it.
  • CloudFront invalidation runs on every deploy rather than relying on cache TTLs, trading a small cost per deploy for an instantly-visible change.