Terraform for a dedicated Google Cloud project that serves Linux packages from a public GCS bucket behind HTTPS, with GitHub Actions write access via Workload Identity Federation.
Only the repositories listed in github_repositories receive:
LIBOPS_PACKAGES_GCLOUD_OIDC_POOLLIBOPS_PACKAGES_GCLOUD_PROJECTLIBOPS_PACKAGES_GCLOUD_REGIONLIBOPS_PACKAGES_GSALIBOPS_PACKAGES_GCS_BUCKETLIBOPS_PACKAGES_PACKAGE_REPO_URLLIBOPS_PACKAGES_APTLY_GPG_KEY_IDLIBOPS_PACKAGES_APTLY_GPG_PRIVATE_KEY_SECRETLIBOPS_PACKAGES_APTLY_GPG_PASSPHRASE_SECRET
If github_actors is non-empty, the Workload Identity provider also restricts access to those actors. Every token must additionally contain a job_workflow_ref from approved_job_workflow_refs. The approved value includes the reusable workflow repository, file, and exact 40-character commit SHA; branch and tag identities are rejected. Repository, optional actor, and exact workflow identity must all match.
libops/sitectl-isle is included in the default publisher allowlist for its v1
release. Removing any repository from the applied github_repositories value
removes that repository's package-publisher WIF binding and managed Actions
variables; it does not alter the repository itself.
approved_job_workflow_refs must cover both active publication paths:
libops/terraform-linux-packages/.github/workflows/reusable-goreleaser.yaml@<sha>for the direct core release workflowlibops/.github/.github/workflows/sitectl-plugin-goreleaser.yaml@<sha>for the shared plugin release workflow
Workflow identity changes and WIF changes cannot be made atomically across repositories. Use this sequence so existing releases remain authorized without granting a mutable identity:
- Merge the replacement reusable workflow and record its exact merge commit SHA.
- Add its exact
job_workflow_reftoapproved_job_workflow_refsin the appliedterraform.tfvars, alongside every currently active SHA. - Run
terraform planand verify that the Workload Identity provider condition adds only the intended exact identity, then apply. - Update direct or plugin caller workflows to pin the newly approved SHA and verify a package publication.
- Remove the superseded workflow identity from
approved_job_workflow_refsand apply again only after every caller has migrated.
For the shared plugin path, merge the shared workflow first, approve its resulting exact SHA here, and only then update plugin callers. This variable is required and intentionally has no Terraform default. The example values are migration anchors for the currently active direct and shared workflows; the applied terraform.tfvars must be updated after each workflow merge.
- Export credentials for both providers:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/admin-creds.json
export GITHUB_TOKEN=ghp_xxx- Review and copy the example variables:
cp terraform.tfvars.example terraform.tfvars- Initialize and apply:
terraform init
terraform plan
terraform apply- Add the Aptly GPG material out of band after Terraform creates the Secret Manager containers:
make create-aptly-gpg-key
make sync-aptly-gpg-key-idmake create-aptly-gpg-key prompts for the passphrase without echoing it and saves the revocation certificate under .out/gpg/.
- Delegate the reported DNS name servers for the package subdomain, for example
packages.libops.io, from the parent DNS zone.
This repo includes a local publishing path that uses the same validation and repository-building logic as GitHub Actions.
Prerequisites:
ghauthenticated for the source GitHub repositorydockergcloudauthenticated for Secret Manager and GCS on the host
By default the local targets use the published tooling image ghcr.io/libops/terraform-linux-packages:main.
To rebuild that image locally instead, run:
make package-tools-image-localExample:
make package GITHUB_REPOSITORY=libops/sitectl PACKAGE_NAME=sitectl RELEASE_VERSION=v1.2.3The publisher currently has no mandatory package exclusions. Only the core
sitectl channel owner may use EXCLUDED_PACKAGE_NAMES to request comma- or
whitespace-separated canonical lowercase package names for an intentional
retirement or cleanup; plugin publishers cannot remove other packages:
make package \
GITHUB_REPOSITORY=libops/sitectl \
PACKAGE_NAME=sitectl \
RELEASE_VERSION=v1.2.3 \
EXCLUDED_PACKAGE_NAMES="sitectl-preview"Every publication validates the current package name and all current .deb and .rpm artifacts before using Google Cloud. An excluded current package or artifact fails closed. After the publisher acquires the channel lock, it synchronizes an exact local snapshot that deletes destination-only stage files, and the complete existing repository must sync successfully; a failed or partial sync stops publication before signing-key access or metadata upload. The exact snapshot prevents a local or retried run from resurrecting stale artifacts. The publisher removes excluded historical artifacts from the staged repository, rebuilds and uploads replacement APT/RPM metadata, and only then deletes the excluded GCS objects. Keeping the exclusion in every publication makes interrupted cleanup self-healing: later publications rediscover and retry any unreferenced object that was not deleted.
When invoked by the core channel owner, the reusable GoReleaser workflow accepts exclusions through its excluded-package-names input; plugin package names are rejected if they request any. GoReleaser runs in a job without OIDC permission. A dependent publication job checks out the exact commit that defines the called workflow, validates the exclusion policy, and builds a local package-tools image from that checkout before cloud authentication. Credentialed publication uses a direct environment-reading wrapper, verifies the unchanged local image ID, and never invokes GNU Make or pulls the mutable main image.
That target will:
- download the
.deband.rpmrelease assets withgh - pull the published Linux tooling image if needed
- fetch the GPG key material from Secret Manager inside the container
- rebuild the Debian and RPM repository metadata inside the container
- sync package assets to the package bucket prefix for that package
- upload repository metadata with
Cache-Control: no-storeand invalidate the Cloud CDN cache for that package prefix
On macOS, this avoids needing native installs of aptly or createrepo_c.
To print or sync the signing key ID from Secret Manager:
make print-aptly-gpg-key-id
make sync-aptly-gpg-key-id- The managed SSL certificate will stay in provisioning until the delegated package subdomain resolves to the created load balancer IP.
- The bucket is public so
aptand other package managers can fetch package metadata and artifacts without authentication. - This stack grants the GitHub service account bucket-level
roles/storage.objectAdminfor syncing package repositories and a custom role withcompute.urlMaps.getandcompute.urlMaps.invalidateCachefor Cloud CDN invalidation. - Cloud CDN uses origin cache headers. Package assets are uploaded as long-lived immutable objects, while mutable repository metadata is uploaded as
no-store. - Terraform creates the Secret Manager secret containers, but it does not write the private key or passphrase into Terraform state.
Below are the commands ran to get packages.libops.io setup using this repo
publish the latest version for our utils
terraform init
terraform apply
make create-aptly-gpg-key GCLOUD_PROJECT=libops-linux-packages
make sync-aptly-gpg-key-id
terraform apply
make package \
GITHUB_REPOSITORY=libops/sitectl \
PACKAGE_NAME=sitectl \
RELEASE_VERSION=v0.10.1
make package \
GITHUB_REPOSITORY=libops/sitectl-drupal \
PACKAGE_NAME=sitectl-drupal \
RELEASE_VERSION=v0.0.4| Name | Version |
|---|---|
| terraform | >= 1.2.4 |
| github | 6.12.1 |
| 7.35.0 |
| Name | Version |
|---|---|
| github.libops | 6.12.1 |
| 7.35.0 |
No modules.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| approved_job_workflow_refs | Exact reusable-workflow identities allowed to publish packages. Keep active direct and shared workflow SHAs during migrations; branch and tag refs are rejected. | set(string) |
n/a | yes |
| aptly_gpg_key_id | GPG key ID Aptly uses to sign the published repository. | string |
"" |
no |
| aptly_gpg_passphrase_secret_id | Secret Manager secret ID that stores the Aptly GPG key passphrase. | string |
"aptly-gpg-passphrase" |
no |
| aptly_gpg_private_key_secret_id | Secret Manager secret ID that stores the armored Aptly private key. | string |
"aptly-gpg-private-key" |
no |
| billing_account | Google Cloud billing account ID. | string |
n/a | yes |
| bucket_location | Bucket location. | string |
"US" |
no |
| bucket_name | Name of the public package bucket. | string |
"libops-linux-packages" |
no |
| dns_zone_dns_name | DNS suffix managed by the zone, with trailing dot. | string |
"packages.libops.io." |
no |
| dns_zone_name | Cloud DNS managed zone name. | string |
"packages-libops-io" |
no |
| github_actors | Optional GitHub actors allowed to use the provider. Leave empty to allow any actor from the approved repositories. | set(string) |
[] |
no |
| github_owner | GitHub organization that owns the repositories allowed to publish packages. | string |
"libops" |
no |
| github_repositories | Full GitHub repository names allowed to impersonate the publishing service account. | set(string) |
[ |
no |
| org_id | Google Cloud organization ID. | string |
n/a | yes |
| package_domain | Fully qualified domain name that will serve the package repository. | string |
"packages.libops.io" |
no |
| project_id | Google Cloud project ID. | string |
"libops-linux-packages" |
no |
| project_name | Google Cloud project display name. | string |
"libops-linux-packages" |
no |
| region | Default Google Cloud region. | string |
"us-east5" |
no |
| Name | Description |
|---|---|
| aptly_gpg_passphrase_secret_id | Secret Manager secret ID for the Aptly key passphrase. |
| aptly_gpg_private_key_secret_id | Secret Manager secret ID for the armored Aptly private key. |
| bucket_name | Package bucket name. |
| dns_name_servers | Name servers assigned to the managed zone. Delegate the zone at your registrar or parent DNS. |
| github_service_account_email | Service account email used by GitHub Actions. |
| package_url | Public HTTPS package repository URL. |
| project_id | Google Cloud project ID. |
| workload_identity_provider | Full Workload Identity Provider resource name for GitHub Actions auth. |