Catatan dari produksi
CI/CDGitHub ActionsCloudflare

CI/CD: GitHub Actions → Cloudflare Pages

Kode Anda tinggal di repo GitHub privat (harryosmar/pangaea.id), dan setiap perubahan mengalir lewat Pull Request. Inilah bagian yang mengubah "PR ter-merge" jadi "situs live ter-update" — rangkaian CI/CD-nya, ujung ke ujung, lengkap dengan tiap perintah yang benar-benar Anda jalankan.

Satu fitur = satu Pull Request

Tak ada yang dirilis langsung ke main. Setiap perubahan dimulai dari sebuah branch, lalu dibuka sebagai PR, dan baru ter-merge setelah semua cek hijau. Merge hijau itulah yang menerbitkan situs.

feature branchfeature/<name>buka Pull Requestgh pr creategate CI — build + typecheck+ preview PR · merge hanya kalau hijausquash-merge → mainsatu fitur = satu commit di maindeploy → Pages (live)
Satu fitur = satu Pull Request. Tiap PR mem-build dan typecheck; hanya merge hijau ke main yang menerbitkan situs.

Dua cara menyambungkan Pages

Ada dua cara mendapatkan deploy Cloudflare Pages. Keduanya jalan; kami pakai yang kedua, dan alasannya penting.

Cara mudah — integrasi Git native

Cara yang kami pakai — GitHub Actions

Repo sudah berisi .github/workflows/deploy.yml: ia menjalankan build + typecheck di tiap PR, dan baru men-deploy ke Pages setelah Anda menambahkan dua secret repository. Sebelum keduanya ada, workflow tetap hijau tapi dorman — jadi Anda bisa memakai integrasi native dulu sekarang dan beralih nanti.

File workflow-nya, dijelaskan

Seluruh pipeline ada di satu file yang sudah ikut dalam repo — .github/workflows/deploy.yml. Ini dia, sedikit dipangkas:

name: Deploy
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read              # least-privilege: the job only reads the repo

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    env:
      CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run typecheck
      - name: Build
        run: npm run build      # prerender → dist/ + csp-hash.mjs (its last step)
        env:
          VITE_GA_ID: ${{ vars.VITE_GA_ID }}

      # Deploy ONLY on a push to main, and only once the token secret exists.
      - name: Deploy to Cloudflare Pages
        if: ${{ github.event_name == 'push' && env.CF_API_TOKEN != '' }}
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy apps/labs/dist --project-name=pangaea-id --branch=main

      # After a real deploy, ping IndexNow so Bing/Yandex recrawl within minutes.
      - name: Notify IndexNow (Bing/Yandex)
        if: ${{ github.event_name == 'push' && env.CF_API_TOKEN != '' }}
        continue-on-error: true
        run: npm run indexnow -w @pangaea/labs

Langkah demi langkah:

  • on: — job berjalan di tiap push ke main dan tiap pull request ke main.
  • permissions: contents: read — token GitHub job ini hanya bisa membaca repo; tak lebih.
  • env: CF_API_TOKEN — memunculkan secret sekali agar langkah deploy bisa menguji apakah ia terisi.
  • checkout + setup-node — clone repo dan pasang Node 20 dengan cache npm.
  • npm ci — install bersih sesuai lockfile (reproducible, beda dari npm install).
  • npm run typecheck — gate pertama. Error tipe menggagalkan job dan memblok PR.
  • Buildnpm run build — mem-prerender tiap halaman ke dist/ dan menjalankan csp-hash.mjs di langkah terakhir (alasan kami build di sini, bukan di container Cloudflare). VITE_GA_ID datang dari GitHub Actions Variable (publik by design; kalau kosong = GA4 tetap no-op).
  • Deploy … — gate yang menentukan: if: github.event_name == 'push' && env.CF_API_TOKEN != ''. Jadi PR mem-build + typecheck tapi tak pernah men-deploy, dan selama secret-nya belum ada, langkah ini cuma dilewati (job tetap hijau). Begitu ia jalan, wrangler-action mengunggah dist/ ke project Pages pangaea-id.
  • Notify IndexNow — gate yang sama, plus continue-on-error: true. Setelah deploy sungguhan, ia mem-ping Bing/Yandex agar halaman baru atau yang berubah di-crawl ulang dalam hitungan menit; kegagalan sesaat tak pernah membuat deploy yang sehat ikut ditandai merah.

Rangkai deploy-nya: token → secret → Actions → Pages → domain

Ini jalur yang benar-benar dipakai situs. GitHub Actions mem-build (jadi csp-hash.mjs selalu jalan) dan mengunggah ke Pages dengan wrangler. Satu push ke main = satu deploy live.

Langkah 1 — Buat project Pages (sekali)

wrangler pages deploy tidak membuat project secara otomatis — ia akan error Project not found [8000007]. Jadi buat dulu sekali saja. Bisa lewat dashboard (Workers & Pages → Create → Pages → Use direct upload, bukan "Connect to Git" → beri nama persis pangaea-id, yang harus sama dengan --project-name di workflow → Create), atau dari CLI:

npx wrangler login   # membuka OAuth di browser, sekali saja
npx wrangler pages project create pangaea-id --production-branch=main

Langkah 2 — Buat token API hak-minimal

Profile → API Tokens → Create Token → Create Custom Token, persis:

  • Token namegithub-actions-pages-deploy
  • PermissionAccount · Cloudflare Pages · Edit (yang ini saja, tak ada lagi)
  • Account ResourcesInclude · akun Anda

Lalu Continue → Create, dan salin token sekarang — Cloudflare menampilkannya sekali saja.

Do

  • Buat Custom Token dengan satu permission Cloudflare Pages · Edit saja
  • Batasi ke satu akun Anda, dan salin nilainya segera

Don't

  • Memberi DNS / Zone / Workers / SSLpages deploy tak membutuhkannya, dan token yang sempit membatasi dampaknya kalau sampai bocor
  • Memakai template jadi (mis. "Edit Cloudflare Workers" — itu pilihan yang keliru, cakupannya terlalu luas)

Langkah 3 — Ambil Account ID Anda

Workers & Pages → sidebar kanan → Account ID (string hex 32 karakter).

Langkah 4 — Tambah dua secret GitHub

Repo → Settings → Secrets and variables → Actions → tab Secrets (bukan Variables) → New repository secret. Nama harus cocok persis:

  • CLOUDFLARE_API_TOKEN — token dari Langkah 2
  • CLOUDFLARE_ACCOUNT_ID — ID dari Langkah 3

Langkah 5 — Deploy

Push atau merge ke main. Pantau GitHub → Actions → Deploy: langkah "Deploy … to Cloudflare Pages" berubah dari skipped ke success, dan pangaea-id.pages.dev pun live. Langkah terakhir "Notify IndexNow" lalu mem-ping Bing/Yandex agar halaman baru atau yang berubah di-crawl ulang dalam hitungan menit — sifatnya best-effort (continue-on-error), dan ia hanya jalan setelah deploy sungguhan.

Langkah 6 — Arahkan domain ke sana

Di Pages project → Custom domains → Set up a domain, tambahkan www.pangaea.id dan pangaea.id (langkah ini menukar record parkir dengan record proxied yang benar). Lalu tambahkan redirect apex → www 301 — lihat Bagian 3 · Root → www.

Contekan

Seluruh pipeline dalam satu baris:

merge PR → Actions build (npm run build → csp-hash) → wrangler pages deploy → npm run indexnow → live di www.pangaea.id dalam ~30d

Roll back kapan saja di Pages → Deployments → Rollback (instan, tanpa build ulang). Untuk memverifikasi rangkaiannya, read-only:

gh run list --branch main --limit 1      # run "Deploy" terbaru harusnya: success
curl -sI https://pangaea-id.pages.dev/   # 200 begitu project punya satu deployment

Troubleshooting: deploy pertama bilang "project not found"

npx wrangler login   # kalau belum login
npx wrangler pages project create pangaea-id --production-branch=main
npx wrangler pages deploy apps/labs/dist --project-name=pangaea-id

Belum yakin project-nya sudah ada? Lihat dulu daftar project Anda — kalau pangaea-id ada di situ, lewati langkah create dan langsung deploy (atau push ke main):

npx wrangler pages project list

Jangan

  • Terus mengulang deploy berharap berhasil. "Not found" berarti project-nya memang belum ada — buat sekali, lalu deploy.

Pertanyaan umum

Bagaimana situs benar-benar dirilis saat Pull Request di-merge?

Merge ke main memicu pipeline deploy: GitHub Actions membangun situs (npm run build, lalu langkah pasca-build wajib csp-hash.mjs yang mengisi hash CSP asli ke _headers), mengunggah hasilnya ke Cloudflare Pages dengan wrangler, lalu menjalankan satu ping IndexNow terakhir (npm run indexnow) agar Bing/Yandex meng-crawl ulang halaman yang berubah dalam hitungan menit. Satu push ke main sama dengan satu deploy live, biasanya sekitar 30 detik. (Token API dan dua secret repository dirangkai di bagian atas.)

Kenapa GitHub Actions, bukan "Connect to Git" milik Cloudflare?

Karena integrasi Git bawaan membangun di dalam container Cloudflare sendiri dan akan melewati langkah csp-hash.mjs repo, sehingga hash CSP-nya salah. Ia juga pintu satu arah — begitu sebuah project memakai integrasi Git, Anda tak bisa mengembalikannya ke direct upload. Jadi build tetap di GitHub Actions, yang menjalankan langkah pasca-build lalu men-deploy.

Ada yang salah terkirim — bagaimana membatalkannya?

Roll back seketika di Pages → Deployments → Rollback. Ia mengarahkan ulang situs live ke deployment sukses sebelumnya tanpa build ulang, jadi pemulihannya langsung.

Berikutnya

Pipeline ini merilis situs statis yang domainnya diarahkan Bagian 1. Cerita bahasa-sederhana kenapa Cloudflare Pages alih-alih VPS sewaan ada di catatan build kami, Rilis — Git → CI/CD → Pages; serah-terima DNS yang lebih dulu ada di Arahkan domain ke Cloudflare (DNS).

Sources

  1. cloudflare/wrangler-action — deploy dari GitHub Actions
  2. Cloudflare Pages — Git integration vs Direct Upload
  3. Cloudflare — permission token API (Pages: Edit)