Add GitHub Artifact Registry publishing (#39)
Some checks are pending
CI / lint-and-test (push) Waiting to run

This commit is contained in:
twentyOne2x 2026-07-07 11:20:09 +02:00 committed by GitHub
parent 630a1f6ea7
commit d89ee7565a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 183 additions and 1 deletions

75
.github/workflows/gcp-artifact.yml vendored Normal file
View file

@ -0,0 +1,75 @@
name: gcp-artifact
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
id-token: write
concurrency:
group: gcp-artifact-${{ github.ref }}
cancel-in-progress: true
env:
PROJECT_ID: teleo-501523
REGION: europe-west6
ARTIFACT_REPOSITORY: teleo
IMAGE_NAME: teleo-pipeline-gcp-staging
WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github
ARTIFACT_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com
jobs:
build-smoke-push:
name: Build, smoke-run, and push Docker image
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.ARTIFACT_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- name: Configure Artifact Registry Docker auth
run: |
gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
- name: Build, smoke-run, and push
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_SHA::7}"
image_uri="${REGION}-docker.pkg.dev/${PROJECT_ID}/${ARTIFACT_REPOSITORY}/${IMAGE_NAME}:${tag}"
docker build \
-f Dockerfile.gcp-staging \
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
--label "livingip.revision=${GITHUB_SHA}" \
--label "livingip.surface=teleo-infrastructure" \
--label "livingip.tier=gcp-staging" \
-t "${image_uri}" \
.
docker run --rm "${image_uri}"
docker push "${image_uri}"
{
echo "image_uri=${image_uri}"
echo "revision=${GITHUB_SHA}"
} > gcp-artifact-image.txt
- name: Upload image receipt
uses: actions/upload-artifact@v4
with:
name: gcp-artifact-image
path: gcp-artifact-image.txt
if-no-files-found: error

View file

@ -15,6 +15,11 @@ This records the current GCP hardening contract for `teleo-501523`.
- `roles/artifactregistry.writer`
- `roles/logging.logWriter`
- `roles/storage.objectViewer`
- GitHub Actions can publish Artifact Registry images through Workload Identity Federation:
- workflow: `.github/workflows/gcp-artifact.yml`
- provider: `projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github`
- service account: `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com`
- repository scope: `living-ip/teleo-infrastructure`
- Backup buckets are versioned and use uniform bucket-level access:
- `gs://teleo-501523-prod-backups`
- `gs://teleo-501523-leoclean-backups`
@ -24,6 +29,16 @@ This records the current GCP hardening contract for `teleo-501523`.
## How To Build
Automatic Artifact Registry publishing runs on pushes to `main` through GitHub Actions. To run the same lane manually from GitHub:
```bash
gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref main
```
The workflow authenticates to GCP with Workload Identity Federation, builds `Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and uploads `gcp-artifact-image.txt` as a run artifact.
For a local/manual Cloud Build proof:
```bash
REVISION="$(git rev-parse HEAD)"
TAG="$(git rev-parse --short=7 HEAD)-manual-$(date -u +%Y%m%d-%H%M%S)"
@ -54,6 +69,7 @@ The check is read-only and prints no secret values. It verifies:
- Artifact Registry immutability and vulnerability scanning.
- Cloud Build config contract.
- Dedicated Cloud Build service account and roles.
- GitHub Actions WIF Artifact Registry publishing contract.
- Compute disk snapshot policy attachment.
- Backup bucket versioning and uniform access.
- Whether Cloud SQL/Postgres exists.
@ -61,7 +77,7 @@ The check is read-only and prints no secret values. It verifies:
## Current Boundaries
The GCP Docker build/publish path is live and proved manually. Automatic Git-triggered CI/CD is not yet live because this project currently has no Cloud Build repository connection or trigger.
The GCP Docker build/publish path is live through manual Cloud Build and GitHub Actions Workload Identity Federation. Native Cloud Build GitHub triggers are not configured because this project currently has no Cloud Build repository connection.
Database redundancy is not complete. The current project has backup buckets and VM disk snapshots, but it does not yet have a GCP Cloud SQL/Postgres pgvector standby or primary. Do not claim DB parity until one of these is true:

View file

@ -28,6 +28,10 @@ CLOUDBUILD_REQUIRED_ROLES = (
"roles/logging.logWriter",
"roles/storage.objectViewer",
)
GITHUB_ARTIFACT_SERVICE_ACCOUNT = f"sa-artifact-builder@{PROJECT}.iam.gserviceaccount.com"
GITHUB_WIF_POOL = "github-actions"
GITHUB_WIF_PROVIDER = "living-ip-github"
GITHUB_REPOSITORY = "living-ip/teleo-infrastructure"
@dataclass
@ -151,6 +155,92 @@ def check_cloud_build_service_account() -> Check:
)
def check_github_actions_artifact_ci() -> Check:
workflow = Path(".github/workflows/gcp-artifact.yml")
if not workflow.exists():
return Check(
"github_actions_artifact_ci",
"blocked",
"missing .github/workflows/gcp-artifact.yml",
required_tier="T3_live_readonly",
current_tier="T0_spec",
)
text = workflow.read_text()
required_text = [
"google-github-actions/auth@v2",
"workloadIdentityPools/github-actions/providers/living-ip-github",
GITHUB_ARTIFACT_SERVICE_ACCOUNT,
"docker run --rm",
"docker push",
]
absent = [item for item in required_text if item not in text]
if absent:
return Check("github_actions_artifact_ci", "fail", f"workflow_missing={absent}")
project_number = run_text(["gcloud", "projects", "describe", PROJECT, "--format=value(projectNumber)"])
provider = run_json(
[
"gcloud",
"iam",
"workload-identity-pools",
"providers",
"describe",
GITHUB_WIF_PROVIDER,
"--project",
PROJECT,
"--location",
"global",
"--workload-identity-pool",
GITHUB_WIF_POOL,
"--format=json",
]
)
condition = provider.get("attributeCondition", "")
if GITHUB_REPOSITORY not in condition:
return Check("github_actions_artifact_ci", "fail", f"provider_condition={condition!r}")
service_account_policy = run_json(
[
"gcloud",
"iam",
"service-accounts",
"get-iam-policy",
GITHUB_ARTIFACT_SERVICE_ACCOUNT,
"--project",
PROJECT,
"--format=json",
]
)
expected_member = (
f"principalSet://iam.googleapis.com/projects/{project_number}/locations/global/"
f"workloadIdentityPools/{GITHUB_WIF_POOL}/attribute.repository/{GITHUB_REPOSITORY}"
)
wif_bound = any(
binding.get("role") == "roles/iam.workloadIdentityUser"
and expected_member in binding.get("members", [])
for binding in service_account_policy.get("bindings", [])
)
if not wif_bound:
return Check("github_actions_artifact_ci", "fail", "missing_workloadIdentityUser_binding")
project_policy = run_json(["gcloud", "projects", "get-iam-policy", PROJECT, "--format=json"])
artifact_member = f"serviceAccount:{GITHUB_ARTIFACT_SERVICE_ACCOUNT}"
artifact_writer = any(
binding.get("role") == "roles/artifactregistry.writer"
and artifact_member in binding.get("members", [])
for binding in project_policy.get("bindings", [])
)
if not artifact_writer:
return Check("github_actions_artifact_ci", "fail", "missing_artifactregistry_writer_role")
return Check(
"github_actions_artifact_ci",
"pass",
f"workflow=.github/workflows/gcp-artifact.yml provider={provider.get('name')} service_account={GITHUB_ARTIFACT_SERVICE_ACCOUNT}",
)
def check_snapshot_policy() -> Check:
policy = run_json(
[
@ -253,6 +343,7 @@ def main() -> int:
safe_check("artifact_repositories", check_artifact_repositories),
safe_check("cloud_build_contract", check_cloud_build_contract),
safe_check("cloud_build_service_account", check_cloud_build_service_account),
safe_check("github_actions_artifact_ci", check_github_actions_artifact_ci),
safe_check("compute_disk_snapshots", check_snapshot_policy),
safe_check("backup_buckets", check_backup_buckets),
safe_check("cloud_sql_pgvector", check_cloud_sql),