skip to content
2026-08-30技術系

Cloud RunとR2で自分用のPacmanリポジトリを構築する


前の記事では、Arch Linuxのパッケージをビルドして配布するために作ったKamisatoと、その設計について書きました。

この記事では、Kamisatoを使って自分用のPacmanリポジトリを構築します。パッケージの定義、Terraform、GitHub Actionsは、すべてpacman-repoという1個のGitHubリポジトリに置きます。Pacmanから参照するリポジトリ名はpersonalとします。

構成を考える際は、実際に運用しているalterlinux-repoalterlinux-terraformを基にしました。

記事の下書きと設定の照合にはGPT 5.6 Solを用いています。

構成

パッケージのビルドと配布には、次のコンポーネントを使います。

コンポーネント配置先担当
ayakaGitHub Actionsビルド対象の選定、パッケージのビルドとアップロード
ayatoGoogle Cloud RunPacmanリポジトリと管理APIの公開
Workers KVCloudflareパッケージとリポジトリのメタデータ
R2Cloudflareパッケージファイル
lumineCloudflare Pagesパッケージ一覧とビルド状況の表示

pacman-repoPKGBUILDをpushすると、GitHub Actions上のayakaがビルド対象を決めます。完成したパッケージはayatoへアップロードされ、メタデータはWorkers KV、パッケージファイルはR2へ保存されます。

Pacmanからの要求はayatoが受け付けます。パッケージ本体を取得する際はR2の署名付きURLへ転送し、R2からPacmanへ直接送信します。

lumineayatoの情報を表示するWebフロントエンドです。静的ファイルをCloudflare Pagesへ配置し、ブラウザからayatoのAPIを呼び出します。

完成後のアカウントとリポジトリ

この手順では、次の3個のアカウントを使います。

サービス用途
GitHubpacman-repoとGitHub Actions
Google CloudCloud Run、Secret Manager、Terraform State
CloudflareR2、Workers KV、Pages、DNS

GitHubでは次のリポジトリを使います。

リポジトリ所有者用途
pacman-repo自分のアカウントまたはOrganizationPKGBUILD、Terraform、GitHub Actions
Hayao0819/Kamisato上流ayakaayatolumine、GitHub Actions用のAction

pacman-repoから公開するPacmanリポジトリは次の1個です。

リポジトリアーキテクチャ公開URL
personalx86_64https://repo.example.com/repo/personal/x86_64

クラウドには次のリソースを作ります。example.comは自分のドメインへ置き換えます。

サービス名前の例用途
Cloud Storage<PROJECT_ID>-tfstateTerraform State
Cloud RunayatoPacmanリポジトリとAPI
Cloud Run Jobsayato-migrateメタデータの移行
IAMayato-runayatoの実行
IAMterraform-deployGitHub ActionsからのTerraform実行
Workload Identity PoolgithubGitHub ActionsのOIDC認証
Workload Identity Providerpacman-repo接続元をGitHubのRepository IDとmainブランチで限定
Secret Managerayato-で始まる4個のSecretR2、Workers KV、CI APIキー
R2personal-pacman-packagesパッケージファイル
Workers KVayato-personalメタデータ
Cloudflare Pagespersonal-packageslumine
Cloud Run Domain Mappingrepo.example.comayatoの独自ドメイン
Cloudflare DNSrepo.example.compackages.example.comayatolumineの公開

準備

手元にはGit、Terraform 1.9以降、Google Cloud CLI、GitHub CLI、Dockerを用意します。Nixを使う場合は、alterlinux-terraformflake.nixflake.lockも構成例として利用できます。

Google Cloudでは課金を有効にしたプロジェクトを選び、CloudflareではR2とWorkers KVを利用できるアカウント、独自ドメインのDNS Zoneを用意します。最初にTerraformを適用するGoogle Cloudアカウントには、API、IAM、Cloud Run、Secret Managerを設定する権限が必要です。以降の例では、Google CloudのプロジェクトIDをPROJECT_IDと表記します。

Terraformを手元から適用するため、Google Cloud CLIとApplication Default Credentialsへログインします。

export PROJECT_ID="your-gcp-project-id"
gcloud auth login
gcloud auth application-default login
gcloud config set project "${PROJECT_ID}"

pacman-repoを作成する

GitHubに空のリポジトリを作り、手元へcloneします。OWNERは自分のユーザー名またはOrganization名です。公開範囲は運用方針に合わせて変更してください。

gh repo create OWNER/pacman-repo --public --clone
cd pacman-repo
 
terraform_source="$(mktemp -d)"
git clone https://github.com/FascodeNet/alterlinux-terraform.git "${terraform_source}"
git -C "${terraform_source}" checkout 3329bf0163e6abcaf913e7fc1e0c9df3c3ccfddf
cp -R "${terraform_source}/bootstrap" "${terraform_source}/terraform" .
rm -rf -- "${terraform_source:?}"

最終的な配置は次の表のとおりです。

パス内容
.ayakarc.jsonayakaが読むソースリポジトリの一覧
personal/repo.jsonpersonalのビルド設定
personal/<pkgbase>/PKGBUILDパッケージ定義
personal/<pkgbase>/.SRCINFOGitHub Actionsが読むパッケージ情報
bootstrap/bootstrap.shTerraform State用バケットの作成
terraform/gcpGoogle Cloud APIの設定
terraform/ayatoCloud Run、Secret Manager、サービスアカウント
terraform/lumineCloudflare PagesとDNS
.github/workflows/deploy.ymlインフラとlumineの配置
.github/workflows/build.ymlパッケージのビルドと公開
.github/workflows/srcinfo.yml.SRCINFOの検査
.github/workflows/update.yml上流バージョンの定期確認

Terraformはalterlinux-terraformのコミット3329bf0を基にします。後述する変数とGitHub Actions用の認証設定を加えれば、独自の名前とドメインで適用できます。パッケージ側の小さな構成例はKamisatoexample/ayakaにあります。

Terraformの生成物と認証情報は.gitignoreへ追加します。

.terraform/
*.tfstate
*.tfstate.*
*.tfvars
*.tfvars.json
backend.hcl
gha-creds-*.json
out/*
!out/.gitkeep

personalリポジトリを定義する

.ayakarc.jsonにはPKGBUILDを置くディレクトリと、ローカルビルド時の出力先を記述します。

{
  "builder": {
    "backend": "chroot",
    "devtools": {
      "archbuild": "extra-x86_64-build"
    }
  },
  "repos": [
    {
      "dir": "./personal",
      "destdir": "./out"
    }
  ]
}

personal/repo.jsonには、Pacmanリポジトリの名前、ビルド対象のアーキテクチャ、ビルド用コンテナを記述します。

{
  "name": "personal",
  "maintainer": "Your Name <you@example.com>",
  "url": "https://repo.example.com/repo/personal",
  "build": {
    "repos": [
      {
        "name": "personal",
        "server": "https://repo.example.com/repo/$repo/$arch"
      }
    ],
    "arches": {
      "x86_64": {}
    },
    "image": "ghcr.io/hayao0819/archlinux:$arch",
    "makepkg": {
      "packager": "Your Name <you@example.com>"
    }
  }
}

personalの直下には、pkgbaseごとのディレクトリを作ります。既存のPKGBUILDを入れる場合も、.SRCINFOを生成して一緒にcommitします。

mkdir -p personal/example-package out
touch out/.gitkeep
cp /path/to/PKGBUILD personal/example-package/PKGBUILD
cd personal/example-package
makepkg --printsrcinfo >.SRCINFO
cd ../..

repo.jsonbuild.reposは、ビルド中に依存パッケージを取得するPacmanリポジトリです。ayatoは起動時にpersonalの空のデータベースを作るため、初回ビルドからこのURLを指定できます。personal内の別パッケージへ依存する場合、先に公開されたパッケージも同じURLから取得します。Arch Linux公式リポジトリはビルド用イメージ側のpacman.confから利用します。

Cloudflareに保存先を作る

R2にpersonal-pacman-packagesというバケットを作り、S3互換API用のAccess Key IDとSecret Access Keyを発行します。R2のS3 APIで使うエンドポイントはhttps://<ACCOUNT_ID>.r2.cloudflarestorage.comです。

Workers KVにはayato-personalという名前空間を作り、名前空間IDを控えます。ayatoがメタデータを読み書きできるAPIトークンも発行します。

lumineにはCloudflare PagesとDNSを編集するAPIトークンを使います。必要な権限はAccount Cloudflare Pagesの編集、Zone DNSの編集、Zoneの読み取りです。R2、Workers KV、Pagesでは用途ごとに認証情報を分けて管理します。

この時点で控える値は次のとおりです。

使用先
Cloudflare Account IDR2のエンドポイント、Pages
Cloudflare Zone ID独自ドメインのDNS
R2 Access Key IDayatoのSecret Manager
R2 Secret Access KeyayatoのSecret Manager
Workers KV Namespace IDayatoの環境変数
Workers KV API TokenayatoのSecret Manager
Pages/DNS API TokenTerraform、Wrangler

Terraform Stateを準備する

bootstrap/bootstrap.shは、Stateを保存するCloud Storageバケットを作ります。

./bootstrap/bootstrap.sh "${PROJECT_ID}" asia-northeast1

バケット名は<PROJECT_ID>-tfstateです。Uniform bucket-level access、Public access prevention、オブジェクトのバージョニングが設定されます。

Stateはgcpayatolumineの3個に分けます。各ディレクトリのbackend.hcl.examplebackend.hclへコピーし、同じバケットと別々のprefixを指定します。

bucket = "your-gcp-project-id-tfstate"
prefix = "ayato"

terraform/gcpではprefix = "gcp"terraform/lumineではprefix = "lumine"へ変更します。

TerraformのGCSバックエンドはState lockingに対応しています。オブジェクトのバージョニングを使うと、操作を誤った際に以前のStateを確認できます。

StateにはR2とWorkers KVの認証情報、CI APIキーが入ります。バケットのPublic access preventionを保ち、閲覧権限はTerraformを実行するアカウントに限ります。

Google CloudとGitHub Actionsの認証を準備する

terraform/gcp/terraform.tfvarsにプロジェクトID、リージョン、GitHubのRepository IDを記述します。Repository IDはGitHub APIから取得できます。

gh api repos/OWNER/pacman-repo --jq '.id'

Repository IDはリポジトリ名を変更しても同じ値が使われます。google-github-actions/authのセキュリティ資料でも、OIDCの条件には数値のIDが推奨されています。

project_id           = "your-gcp-project-id"
region               = "asia-northeast1"
github_repository_id = "123456789"

GitHub Actionsからの認証にはWorkload Identity Federationを使います。terraform/gcp/main.tfのAPI一覧へSecurity Token ServiceとIAM Service Account Credentialsを加えます。

locals {
  services = [
    "cloudresourcemanager.googleapis.com",
    "serviceusage.googleapis.com",
    "iam.googleapis.com",
    "iamcredentials.googleapis.com",
    "sts.googleapis.com",
    "storage.googleapis.com",
  ]
}

terraform/gcp/github-actions.tfでは、terraform-deployサービスアカウントとWorkload Identity Poolを作ります。ProviderはRepository IDとmainブランチが一致するOIDCトークンを受け付けます。

resource "google_service_account" "terraform_deploy" {
  account_id   = "terraform-deploy"
  display_name = "Terraform deploy from GitHub Actions"
 
  depends_on = [google_project_service.this]
}
 
locals {
  terraform_deploy_roles = toset([
    "roles/iam.serviceAccountAdmin",
    "roles/iam.serviceAccountUser",
    "roles/run.admin",
    "roles/secretmanager.admin",
    "roles/serviceusage.serviceUsageAdmin",
  ])
}
 
resource "google_project_iam_member" "terraform_deploy" {
  for_each = local.terraform_deploy_roles
 
  project = var.project_id
  role    = each.value
  member  = "serviceAccount:${google_service_account.terraform_deploy.email}"
}
 
resource "google_storage_bucket_iam_member" "terraform_state" {
  bucket = "${var.project_id}-tfstate"
  role   = "roles/storage.objectAdmin"
  member = "serviceAccount:${google_service_account.terraform_deploy.email}"
}
 
resource "google_iam_workload_identity_pool" "github" {
  workload_identity_pool_id = "github"
  display_name              = "GitHub Actions"
 
  depends_on = [google_project_service.this]
}
 
resource "google_iam_workload_identity_pool_provider" "pacman_repo" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id
  workload_identity_pool_provider_id = "pacman-repo"
  display_name                       = "pacman-repo"
 
  attribute_mapping = {
    "google.subject"          = "assertion.sub"
    "attribute.repository_id" = "assertion.repository_id"
    "attribute.ref"           = "assertion.ref"
  }
  attribute_condition = "assertion.repository_id == '${var.github_repository_id}' && assertion.ref == 'refs/heads/main'"
 
  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}
 
resource "google_service_account_iam_member" "github_actions" {
  service_account_id = google_service_account.terraform_deploy.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository_id/${var.github_repository_id}"
}

terraform/gcp/variables.tfへGitHubリポジトリの変数を追加します。

variable "github_repository_id" {
  description = "Numeric GitHub repository ID allowed to run Terraform."
  type        = string
}

Workflowへ渡す値はterraform/gcp/outputs.tfから取り出します。

output "workload_identity_provider" {
  value = google_iam_workload_identity_pool_provider.pacman_repo.name
}
 
output "terraform_deploy_service_account" {
  value = google_service_account.terraform_deploy.email
}

設定を確認してから適用します。

terraform -chdir=terraform/gcp init -backend-config=backend.hcl
terraform -chdir=terraform/gcp plan
terraform -chdir=terraform/gcp apply

この構成ではGoogle CloudのAPI、terraform-deployサービスアカウント、GitHub Actions用のOIDC Providerを用意します。terraform/ayatoを適用すると、Cloud Run Admin APIとSecret Manager APIも有効になります。

適用後、outputをGitHubのRepository Variableへ登録します。

gh variable set GCP_PROJECT_ID --repo OWNER/pacman-repo --body "${PROJECT_ID}"
gh variable set TF_STATE_BUCKET --repo OWNER/pacman-repo --body "${PROJECT_ID}-tfstate"
terraform -chdir=terraform/gcp output -raw workload_identity_provider \
  | gh variable set GCP_WORKLOAD_IDENTITY_PROVIDER --repo OWNER/pacman-repo
terraform -chdir=terraform/gcp output -raw terraform_deploy_service_account \
  | gh variable set GCP_SERVICE_ACCOUNT --repo OWNER/pacman-repo

Workload Identity PoolとIAM設定の反映には数分かかる場合があります。登録直後のWorkflowが認証で失敗した場合は、少し待ってから再実行します。

ayatoをCloud Runへ配置する

terraform/ayatoは、Cloud Runサービスのayato、Cloud Run Jobのayato-migrateayato-runサービスアカウント、4個のSecretを作ります。

personal向けに変数を変更する

terraform/ayato/terraform.tfvars.exampleterraform.tfvarsへコピーし、次の値を設定します。

project_id         = "your-gcp-project-id"
region             = "asia-northeast1"
ayato_image_repo   = "docker.io/hayao0819/kamisato"
ayato_image_tag    = "sha-b0f4d89743f68070e72802533a916e56e1e921bc"
ayato_domains      = ["repo.example.com"]
auth_public_origin = "https://packages.example.com"
 
ayato_repos = [
  { name = "personal", arches = ["x86_64"] },
]
 
cloudflare_account_id = "your-cloudflare-account-id"
r2_bucket_name        = "personal-pacman-packages"
r2_access_key_id      = "your-r2-access-key-id"
r2_secret_access_key  = "your-r2-secret-access-key"
cfkv_namespace_id     = "your-workers-kv-namespace-id"
cfkv_token            = "your-workers-kv-api-token"

ayato_reposは、ayatoが公開するリポジトリと、アップロードを受け付けるアーキテクチャの一覧です。personal/repo.jsonnamebuild.archesを同じ内容にします。

terraform/ayato/secrets.tfにあるCI APIキーの名前も、GitHubリポジトリに合わせます。

locals {
  ci_api_keys_json = jsonencode([{
    name          = "pacman-repo"
    key           = random_password.ci_api_key.result
    publish_repos = [for r in var.ayato_repos : r.name]
  }])
}

CI APIキーは48文字のランダムな値です。R2とWorkers KVの認証情報とともにSecret Managerへ保存され、ayato-runには各Secretを読む権限が付与されます。

Cloud Runの設定を確認する

ayatoAYATO_STORE_DB_TYPE=cfkvAYATO_STORE_STORAGE_TYPE=s3で起動します。R2はリージョンautoのS3互換ストレージとして接続し、Workers KVにリポジトリデータベースの元になるメタデータを保存します。

Cloud Runへ渡すイメージは、TerraformのDocker Providerが解決したダイジェストで固定されます。/healthをstartup probeに使い、startup_cpu_boostcpu_idleを有効にします。

最小インスタンス数は1です。Pacmanの最初の要求がコールドスタート中にタイムアウトする事態を避けるため、待機インスタンスを維持します。Cloud Runの最小インスタンスには待機中の料金が発生します。

Cloud Runサービスにはroles/run.invokerallUsersへ付与します。Pacmanからの読み取りは公開し、パッケージを変更するAPIはCI APIキーで認証します。

Cloud Run Domain Mappingを作る前に、手元でTerraformを適用するGoogleアカウントから、Google Search Consoleで親ドメインの所有権を確認します。terraform-deployが作成されたら、そのサービスアカウントも確認済み所有者へ追加します。

Terraformを適用する

terraform -chdir=terraform/ayato init -backend-config=backend.hcl
terraform -chdir=terraform/ayato plan
terraform -chdir=terraform/ayato apply

適用後、Cloud RunのURL、独自ドメイン用のDNSレコード、CI APIキーをoutputから確認できます。

terraform -chdir=terraform/ayato output -json domain_mapping_dns
terraform -chdir=terraform/ayato output -raw ci_api_key \
  | gh secret set AYATO_TOKEN --repo OWNER/pacman-repo
gh variable set AYATO_URL --repo OWNER/pacman-repo --body "https://repo.example.com"

OWNERはGitHubのユーザー名またはOrganization名に置き換えます。domain_mapping_dnsに表示されたレコードはCloudflare DNSへ追加し、Proxy statusをDNS onlyにします。

lumineをCloudflare Pagesへ配置する

terraform/lumineは、Cloudflare Pagesのプロジェクト、独自ドメイン、DNSレコードを管理します。terraform/lumine/terraform.tfvarsを次のように設定します。

cloudflare_account_id = "your-cloudflare-account-id"
cloudflare_zone_id    = "your-zone-id"
cloudflare_api_token  = "your-pages-and-dns-api-token"
pages_project_name    = "personal-packages"
lumine_domain         = "packages.example.com"

Terraformはpersonal-packagesというPagesプロジェクトを作り、packages.example.comからPagesのサブドメインへProxied CNAMEを設定します。

terraform -chdir=terraform/lumine init -backend-config=backend.hcl
terraform -chdir=terraform/lumine plan
terraform -chdir=terraform/lumine apply

lumineの静的ファイルはKamisatoのbuild-lumine Actionでビルドします。ayato_urlhttps://repo.example.comを渡すと、実行時設定とContent Security PolicyにAPIの接続先が入ります。生成物はWranglerでpersonal-packagesへ配置します。

GitHub Actionsからインフラを更新する

.github/workflows/deploy.ymlは、KamisatoのrefをコミットSHAへ変換し、そのコミットからayatoのイメージとlumineの静的ファイルを配置します。terraform/ayatoterraform/lumineのStateは、同じバケット内でprefixを分けます。

name: Deploy
 
on:
  workflow_dispatch:
    inputs:
      ref:
        description: "配置するKamisatoのref"
        default: b0f4d89743f68070e72802533a916e56e1e921bc
 
concurrency:
  group: deploy
  cancel-in-progress: false
 
permissions:
  contents: read
  id-token: write
 
env:
  KAMISATO_REPO: Hayao0819/Kamisato
  PAGES_PROJECT: personal-packages
  AYATO_URL: https://repo.example.com
 
jobs:
  resolve:
    runs-on: ubuntu-latest
    outputs:
      sha: ${{ steps.resolve.outputs.sha }}
    steps:
      - id: resolve
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          sha=$(gh api "repos/${KAMISATO_REPO}/commits/${{ inputs.ref }}" --jq .sha)
          echo "sha=${sha}" >>"${GITHUB_OUTPUT}"
 
  terraform:
    needs: resolve
    runs-on: ubuntu-latest
    env:
      TF_VAR_ayato_image_tag: sha-${{ needs.resolve.outputs.sha }}
      TF_VAR_project_id: ${{ vars.GCP_PROJECT_ID }}
      TF_VAR_cloudflare_account_id: ${{ vars.CF_ACCOUNT_ID }}
      TF_VAR_cloudflare_zone_id: ${{ vars.CLOUDFLARE_ZONE_ID }}
      TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
      TF_VAR_r2_access_key_id: ${{ secrets.R2_ACCESS_KEY_ID }}
      TF_VAR_r2_secret_access_key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
      TF_VAR_cfkv_namespace_id: ${{ vars.CFKV_NAMESPACE_ID }}
      TF_VAR_cfkv_token: ${{ secrets.CFKV_TOKEN }}
      TF_VAR_r2_bucket_name: personal-pacman-packages
      TF_VAR_ayato_domains: '["repo.example.com"]'
      TF_VAR_auth_public_origin: https://packages.example.com
      TF_VAR_ayato_repos: '[{"name":"personal","arches":["x86_64"]}]'
      TF_VAR_pages_project_name: personal-packages
      TF_VAR_lumine_domain: packages.example.com
    steps:
      - uses: actions/checkout@v7
      - uses: google-github-actions/auth@v3
        with:
          project_id: ${{ vars.GCP_PROJECT_ID }}
          workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
      - uses: hashicorp/setup-terraform@v4
        with:
          terraform_wrapper: false
      - name: Apply ayato
        working-directory: terraform/ayato
        run: |
          terraform init -input=false \
            -backend-config="bucket=${{ vars.TF_STATE_BUCKET }}" \
            -backend-config="prefix=ayato"
          terraform apply -input=false -auto-approve
      - name: Apply lumine
        working-directory: terraform/lumine
        run: |
          terraform init -input=false \
            -backend-config="bucket=${{ vars.TF_STATE_BUCKET }}" \
            -backend-config="prefix=lumine"
          terraform apply -input=false -auto-approve
 
  lumine:
    needs: [resolve, terraform]
    runs-on: ubuntu-latest
    steps:
      - id: build
        uses: Hayao0819/Kamisato/actions/build-lumine@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          ayato_url: ${{ env.AYATO_URL }}
          auth_mode: bearer
          ref: ${{ needs.resolve.outputs.sha }}
      - uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ vars.CF_ACCOUNT_ID }}
          command: >-
            pages deploy ${{ steps.build.outputs.dir }}
            --project-name=${{ env.PAGES_PROJECT }}
            --branch=main

terraform/gcpで作成した認証情報を使うため、Workflowにはid-token: writeを付けています。GitHubのOIDC資料にも、この権限が必要だと記載されています。

CloudflareとStateの値は、GitHub SecretsとRepository Variableから渡します。

種類名前内容
SecretCLOUDFLARE_API_TOKENPagesとDNS用のAPIトークン
SecretR2_ACCESS_KEY_IDR2 Access Key ID
SecretR2_SECRET_ACCESS_KEYR2 Secret Access Key
SecretCFKV_TOKENWorkers KV用のAPIトークン
VariableCF_ACCOUNT_IDCloudflare Account ID
VariableCLOUDFLARE_ZONE_ID独自ドメインのZone ID
VariableCFKV_NAMESPACE_IDWorkers KV Namespace ID
VariableGCP_PROJECT_IDGoogle CloudプロジェクトID
VariableTF_STATE_BUCKET<PROJECT_ID>-tfstate
VariableGCP_WORKLOAD_IDENTITY_PROVIDERWorkload Identity Providerのリソース名
VariableGCP_SERVICE_ACCOUNTterraform-deployのメールアドレス

Cloudflareの値を登録します。Secretのコマンドは標準入力を待つため、表示された入力欄へ値を貼り付けます。

gh variable set CF_ACCOUNT_ID --repo OWNER/pacman-repo --body "your-cloudflare-account-id"
gh variable set CLOUDFLARE_ZONE_ID --repo OWNER/pacman-repo --body "your-zone-id"
gh variable set CFKV_NAMESPACE_ID --repo OWNER/pacman-repo --body "your-workers-kv-namespace-id"
 
gh secret set CLOUDFLARE_API_TOKEN --repo OWNER/pacman-repo
gh secret set R2_ACCESS_KEY_ID --repo OWNER/pacman-repo
gh secret set R2_SECRET_ACCESS_KEY --repo OWNER/pacman-repo
gh secret set CFKV_TOKEN --repo OWNER/pacman-repo

この認証方法は、GitHub Actionsが実行時に発行するOIDCトークンをGoogle Cloudの短期認証情報へ交換します。google-github-actions/authでも、長期間有効なJSON鍵よりWorkload Identity Federationが推奨されています。

初回は手元でTerraformを適用します。以後はmainブランチを選び、deploy.ymlworkflow_dispatchから実行します。パッケージ定義とTerraformの変更履歴、Workflowの実行結果はpacman-repoに残ります。

GitHub Actionsでパッケージを公開する

.github/workflows/build.ymlは、personal以下へのpushと手動実行で起動します。処理はplanbumpbuildpruneの順です。

このWorkflowが使う値は2個です。

種類名前内容
VariableAYATO_URLhttps://repo.example.com
SecretAYATO_TOKENTerraformが生成したCI APIキー
name: Build and publish
 
on:
  push:
    branches: [main]
    paths: ["personal/**"]
  workflow_dispatch:
    inputs:
      packages:
        description: "再ビルドするpkgbaseを空白区切りで指定"
        required: false
        default: ""
      all:
        description: "すべてのパッケージを再ビルド"
        type: boolean
        required: false
        default: false
 
permissions:
  contents: write
 
concurrency:
  group: build-${{ github.ref }}
 
jobs:
  plan:
    runs-on: ubuntu-latest
    outputs:
      build_matrix: ${{ steps.plan.outputs.build_matrix }}
      prune_matrix: ${{ steps.plan.outputs.prune_matrix }}
      bumps: ${{ steps.plan.outputs.bumps }}
      any_build: ${{ steps.plan.outputs.any_build }}
    steps:
      - uses: actions/checkout@v7
        with:
          submodules: recursive
          fetch-depth: 0
      - uses: actions/setup-go@v7
        with:
          go-version: "1.x"
          cache: false
      - uses: Hayao0819/Kamisato/actions/install@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          ayaka: "true"
          version: b0f4d89743f68070e72802533a916e56e1e921bc
      - id: plan
        uses: Hayao0819/Kamisato/actions/plan@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          server: ${{ vars.AYATO_URL }}
          packages: ${{ github.event.inputs.packages }}
          all: ${{ github.event.inputs.all }}
          workers: 4
 
  bump:
    needs: plan
    if: needs.plan.outputs.bumps != '{}'
    runs-on: ubuntu-latest
    container: archlinux:latest
    outputs:
      sha: ${{ steps.bump.outputs.sha }}
    steps:
      - name: Install tooling
        run: pacman -Syu --noconfirm base-devel git go sudo jq
      - uses: actions/checkout@v7
        with:
          submodules: recursive
          fetch-depth: 0
      - uses: Hayao0819/Kamisato/actions/install@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          ayaka: "true"
          version: b0f4d89743f68070e72802533a916e56e1e921bc
      - id: bump
        uses: Hayao0819/Kamisato/actions/bump@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          bumps: ${{ needs.plan.outputs.bumps }}
          ref: main
 
  build:
    needs: [plan, bump]
    if: >-
      always() &&
      needs.plan.result == 'success' &&
      needs.plan.outputs.any_build == 'true' &&
      (needs.bump.result == 'success' || needs.bump.result == 'skipped')
    runs-on: ubuntu-latest
    timeout-minutes: 355
    strategy:
      fail-fast: false
      matrix: ${{ fromJSON(needs.plan.outputs.build_matrix) }}
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ needs.bump.outputs.sha || github.sha }}
          submodules: recursive
          fetch-depth: 0
      - uses: actions/setup-go@v7
        with:
          go-version: "1.x"
          cache: false
      - uses: Hayao0819/Kamisato/actions/install@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          ayaka: "true"
          version: b0f4d89743f68070e72802533a916e56e1e921bc
      - name: Build and publish packages
        uses: Hayao0819/Kamisato/actions/build@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          server: ${{ vars.AYATO_URL }}
          repo: ${{ matrix.repo }}
          arch: ${{ matrix.arch }}
          pkgs: ${{ matrix.pkgs }}
          api_key: ${{ secrets.AYATO_TOKEN }}
 
  prune:
    needs: [plan, bump, build]
    if: >-
      always() &&
      needs.plan.result == 'success' &&
      needs.build.result != 'failure' &&
      needs.build.result != 'cancelled'
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: ${{ fromJSON(needs.plan.outputs.prune_matrix) }}
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ needs.bump.outputs.sha || github.sha }}
          submodules: recursive
          fetch-depth: 0
      - uses: actions/setup-go@v7
        with:
          go-version: "1.x"
          cache: false
      - uses: Hayao0819/Kamisato/actions/install@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          ayaka: "true"
          version: b0f4d89743f68070e72802533a916e56e1e921bc
      - name: Prune removed packages
        uses: Hayao0819/Kamisato/actions/prune@b0f4d89743f68070e72802533a916e56e1e921bc
        with:
          server: ${{ vars.AYATO_URL }}
          repo: ${{ matrix.repo }}
          arch: ${{ matrix.arch }}
          api_key: ${{ secrets.AYATO_TOKEN }}

KamisatoのActionは、記事執筆時のコミットb0f4d89743f68070e72802533a916e56e1e921bcへ固定しました。元にしたWorkflowはalterlinux-repobuild.ymlです。

plan

planpersonalのソースとayatoで配布中の内容を比較し、ビルド行列を作ります。パッケージのバージョンに加えてmakedependsの連鎖も調べ、再ビルドが必要なパッケージを同じ計画に含めます。

初回はpackagesを空、allをfalseにして実行します。ayaka planが配布前のパッケージを選びます。allは全パッケージを作り直すときに使います。

bump

依存パッケージの更新に伴う再ビルドでは、bumpが対象のpkgrelを増やしてmainへpushします。Workflowのcontents: writeはこの処理で使います。同じブランチの実行はconcurrencyで直列化します。

build

buildは計画された行列ごとにGitHub-hosted runnerを起動します。ayakaが依存されるパッケージから順にビルドし、完成するたびにayatoへ公開します。後続のビルドはpersonal/repo.jsonに書いたURLから新しい依存パッケージを取得できます。

prune

pruneはソースに残っているPKGBUILDと配布中の内容を比較し、削除されたパッケージをayatoから取り除きます。ビルド対象が0件の回にも実行されます。

.SRCINFOを検査する

.github/workflows/srcinfo.ymlから、Kamisatoの再利用可能なWorkflowを呼び出します。

name: Check .SRCINFO
 
on:
  pull_request:
    paths: ["personal/**"]
  push:
    branches: [main]
    paths: ["personal/**"]
 
permissions:
  contents: read
 
jobs:
  srcinfo:
    uses: Hayao0819/Kamisato/.github/workflows/srcinfo-check.yml@b0f4d89743f68070e72802533a916e56e1e921bc
    with:
      repos: personal
      version: b0f4d89743f68070e72802533a916e56e1e921bc

例では記事執筆時のKamisatoのコミットb0f4d89743f68070e72802533a916e56e1e921bcへ固定しています。このWorkflowは.SRCINFOを再生成し、commit済みの内容との差を検査します。

上流の更新を取り込む

.nvchecker.tomlを持つパッケージは、Kamisatoのupstream-update.ymlで更新を確認できます。

name: Update upstream versions
 
on:
  schedule:
    - cron: "0 3 * * *"
  workflow_dispatch: {}
 
permissions:
  contents: write
  actions: write
 
concurrency:
  group: build-refs/heads/main
 
jobs:
  update:
    uses: Hayao0819/Kamisato/.github/workflows/upstream-update.yml@b0f4d89743f68070e72802533a916e56e1e921bc
    with:
      version: b0f4d89743f68070e72802533a916e56e1e921bc
      ref: main

更新用WorkflowはPKGBUILD.SRCINFOを変更してpushし、その後にbuild.ymlをdispatchします。GitHub Actionsの仕様では、GITHUB_TOKENによるpushから別のWorkflowが起動しないためです。

Workflowを揃えたら、既定ブランチをmainとしてpushします。

git add .
git commit -m "feat: Add initial repository configuration"
git branch -M main
git push -u origin main

最初のビルドと動作確認

最初のpushにはpersonal以下のファイルが含まれるため、Build and publishが自動で起動します。手動で実行し直す場合はpackagesを空、allをfalseにします。

push後にmainブランチからDeployも実行します。Terraformの再適用に続いてlumineがCloudflare Pagesへ配置されます。

先にayato/healthを確認します。

curl --fail --show-error https://repo.example.com/health

ビルド後はPacmanリポジトリのデータベースを取得します。R2への転送をたどるため、--locationを付けます。

curl --fail --location --output /dev/null \
  --write-out '%{http_code}\n' \
  https://repo.example.com/repo/personal/x86_64/personal.db

どちらもHTTP 200を返せば、ayatoとR2まで到達しています。

Pacmanから使う端末では、/etc/pacman.confへ次の設定を追加します。

[personal]
SigLevel = Optional TrustAll
Server = https://repo.example.com/repo/$repo/$arch

設定後、データベースを更新してパッケージを確認します。

sudo pacman -Syu
pacman -Sl personal

luminehttps://packages.example.comで開きます。パッケージ一覧、検索、ビルド状況が表示され、API要求はhttps://repo.example.comへ送られます。

運用時に確認すること

この記事で使ったCloud Run Domain MappingはPreviewで、利用できるリージョンも限られています。Google Cloudは本番環境の独自ドメインにGlobal external Application Load Balancerを推奨しています。個人用の範囲を越えて運用する場合は、独自ドメインの構成を先に見直します。

Workers KVは結果整合のストレージです。Cloudflareの資料では、書き込みが別の拠点から見えるまで60秒以上かかる場合があると説明されています。公開直後の確認では、参照する拠点によって以前のメタデータが返ることがあります。

現在のPacman設定はSigLevel = Optional TrustAllです。Kamisatoの署名機能を使う場合は、署名鍵の運用と利用端末への公開鍵の配布を決めたうえで、Pacman側のSigLevelを変更します。