Tag: DevOps

  • How to set up CI/CD with GitHub Actions

    How to set up CI/CD with GitHub Actions

    GitHub Actions gets a bad reputation because people copy a giant YAML file from a blog post, it half works, and they never touch it again until it breaks. I want to show you the small, understandable version that I actually run on real projects.

    Where workflows live

    Every workflow is a YAML file inside .github/workflows. The directory name is not optional. Each file describes one or more jobs, and each job runs on a fresh virtual machine. The mental model that helped me most: a job is a clean laptop that boots, does exactly what you tell it, and then evaporates. Nothing persists between jobs unless you explicitly save it.

    A minimal but real pipeline

    Here is a workflow that runs on every push and pull request. It installs dependencies, runs the test suite, and builds the site. Note how the trigger, the runner, and the steps map to plain English:

    name: CI
    on:
      push:
        branches: [main]
      pull_request:
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: npm
          - run: npm ci
          - run: npm test
          - run: npm run build

    Two things here earn their keep. The cache: npm line restores your dependency cache so installs drop from a minute to a few seconds. And npm ci instead of npm install respects your lockfile exactly, which means CI installs the same versions every time. Reproducibility is the whole point.

    Run jobs in parallel when you can

    If your lint, test, and type-check steps do not depend on each other, do not chain them. Split them into separate jobs and they run at the same time on different machines. Your feedback loop gets shorter, and the cost is the same since you pay for compute minutes either way. I only force sequence with needs when a later job genuinely requires an earlier one to finish, like deploy waiting on test.

    Adding deployment safely

    This is where I see the most mistakes. Deployment should only run on the main branch, never on pull requests, and it should depend on tests passing. The shape looks like this:

      deploy:
        needs: build
        if: github.ref == 'refs/heads/main'
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - run: npm ci
          - run: npm run build
          - name: Publish
            run: npx wrangler pages deploy dist --project-name my-site

    Wrangler needs a Cloudflare API token to publish. Store it as an encrypted repository secret under Settings, Secrets and variables, and expose it to the step through the job’s environment block. Never paste a token into the YAML itself, because the file is in your history forever. If you are deploying to Cloudflare specifically, the manual setup side is covered in deploying a static site to Cloudflare Pages.

    Secrets, the right way

    Repository secrets are encrypted and only decrypted at runtime inside the job. They are masked in logs, so if a token accidentally prints, GitHub redacts it. Reference them through the secrets context in your workflow’s environment mapping rather than echoing them. The golden rule: if it would let someone deploy or spend money, it is a secret, and it lives in GitHub’s secret store, not your code.

    Caching beyond dependencies

    You can cache more than node_modules. Build artifacts, compiled binaries, downloaded assets, anything expensive to recreate is a candidate. The actions/cache step takes a key, usually a hash of a lockfile or source directory, and restores the matching cache if it exists. Get the key right and your slow steps become instant. Get it wrong and you ship stale artifacts, so always include the relevant file hash in the key.

    Keep it boring

    My strongest advice is to resist cleverness. A workflow that anyone on the team can read in thirty seconds is worth more than a brilliant one only you understand. Add steps when you have a concrete reason, delete them the moment they stop pulling their weight, and pin your action versions so a surprise update never breaks a Friday deploy. If your pipeline also publishes content that needs to be searchable, you can fold that step in too, which pairs nicely with adding full-text search to a static site. Done well, CI/CD fades into the background and just keeps your main branch deployable.

  • How to deploy a static site to Cloudflare Pages

    How to deploy a static site to Cloudflare Pages

    I have shipped a lot of sites onto Cloudflare Pages, including the one you are reading right now. The reason I keep coming back is boring in the best way: it is fast, the free tier is generous, and once it is set up I rarely think about it again. Here is how I do it.

    Connect the repository first

    Pages is happiest when it builds from a Git repo. Sign in to the Cloudflare dashboard, open Workers and Pages, and pick “Create application” then “Pages”. Authorize GitHub or GitLab, select your repository, and you land on the build configuration screen. This is the part people get wrong, so slow down here.

    You need three things: the framework preset (or “None” for a hand-rolled build), the build command, and the output directory. For my own generator the build command is node build.js and the output directory is dist. If you are using a known tool, the presets fill these in for you. If your “build” is just copying files, set the command to something harmless like echo done and point the output at your folder.

    Match the Node version to your local one

    A surprising number of failed first deploys come down to Node mismatches. Pages defaults to a recent Node, but your code might assume something else. I pin it explicitly with an environment variable so there are no surprises:

    NODE_VERSION = 20.11.0

    Add that under Settings, Environment variables, for both Production and Preview. While you are there, add any other secrets your build needs, like API tokens for pulling content. Anything sensitive belongs here, never in the repo.

    Trigger the first build

    Hit “Save and Deploy” and watch the log stream. The first build is the honest one. If a dependency is missing or your output directory is wrong, you will see it immediately. A clean build ends with Pages uploading your files to its edge network, and you get a *.pages.dev URL to test against. Open it, click around, and confirm assets actually load. Broken relative paths are the most common issue, usually from a site that assumed it lived at a subpath.

    Add your custom domain

    Once the preview looks right, attach a real domain. Go to the project’s Custom domains tab and add your hostname. If the domain is already on Cloudflare, the DNS record is created for you in one click. If it lives elsewhere, you will get a CNAME to add at your registrar. Propagation is usually minutes, not hours. Cloudflare provisions the TLS certificate automatically, so HTTPS just works without you touching certbot.

    Set up redirects and headers

    Static sites still need rules. Pages reads two special files from your output directory. A _redirects file handles URL rewrites and old links, and a _headers file lets you set caching and security headers. Here is a small example that locks down framing and caches assets hard:

    /*
      X-Frame-Options: DENY
      Referrer-Policy: strict-origin-when-cross-origin
    
    /assets/*
      Cache-Control: public, max-age=31536000, immutable

    Drop those in the folder you ship, not the project root, unless your build copies them across. Aggressive caching on hashed asset filenames is one of the cheapest performance wins you will ever get.

    Automate redeploys

    Every push to your production branch triggers a fresh build, and pull requests get their own preview URLs automatically. That alone covers most workflows. But if your content lives outside the repo, in a CMS for example, you will want a build hook. Create one under Settings, Builds and deployments, and you get a URL you can POST to from anywhere to kick off a deploy. I wire mine to a webhook so editors never touch Git.

    If you want more control over the build pipeline, you can skip the dashboard build entirely and run it yourself. I cover that approach in setting up CI/CD with GitHub Actions, which lets you deploy with the Wrangler CLI after your own test and lint steps pass.

    What I check before calling it done

    Before I trust a deploy, I run through a short list. Does the custom domain serve over HTTPS with no mixed-content warnings? Do the redirects actually fire? Are large images sensible, or am I shipping 4MB hero photos? That last one matters more than people expect, and I wrote up my whole approach in optimizing images for the web.

    That is genuinely it. Cloudflare Pages rewards a simple setup, and the edge network means your visitors in Sydney get the same snappy load as the ones next door. Once the pipeline is in place, deploying becomes a non-event, which is exactly what you want.