From 3e1b751066bc0bc3ecaa8b9bb7ed1617af12772e Mon Sep 17 00:00:00 2001 From: "Zeyu (Zayne) Zhang" Date: Sat, 8 Aug 2026 17:28:37 +0800 Subject: [PATCH 1/4] ci: automate safe-settings deployment --- .github/workflows/deploy-k8s.yml | 164 +++++++++++++++--------- DEPLOY.md | 116 +++++++++++------ helm/safe-settings/values-hacktron.yaml | 24 ++++ script/bootstrap-k8s-secret | 50 ++++++++ 4 files changed, 255 insertions(+), 99 deletions(-) create mode 100644 helm/safe-settings/values-hacktron.yaml create mode 100755 script/bootstrap-k8s-secret diff --git a/.github/workflows/deploy-k8s.yml b/.github/workflows/deploy-k8s.yml index 7bfb4332b..0d91a2f27 100644 --- a/.github/workflows/deploy-k8s.yml +++ b/.github/workflows/deploy-k8s.yml @@ -1,70 +1,114 @@ -name: Deploy to Kubernetes -on: +name: Deploy safe-settings + +on: + push: + branches: + - main-enterprise + paths: + - .github/workflows/deploy-k8s.yml + - Dockerfile + - index.js + - lib/** + - package.json + - package-lock.json + - helm/safe-settings/** workflow_dispatch: - inputs: - status: - description: 'Status of the previous workflow' - required: true - default: 'passed' - release: - description: 'Release tag from the previous workflow' - required: true -env: - DOCKER_IMAGE_NAME: yadhav/safe-settings - #IMAGE_REGISTRY_URL: docker.pkg.github.com - IMAGE_REGISTRY_URL: docker.io - AZURE_RESOURCE_GROUP: decyjphr-aks - AZURE_AKS_CLUSTER: decyjphr-aks - AZURE_LOCATION: '"East US"' - AZURE_AKS_NAMESPACE: default permissions: - id-token: write - + contents: read + +concurrency: + group: safe-settings-production + cancel-in-progress: false + +env: + GCP_PROJECT_ID: hacktron-462816 + GCP_REGION: us-central1 + ARTIFACT_REPOSITORY: safe-settings + IMAGE_NAME: github-app + GKE_CLUSTER: safe-settings-cluster + GKE_LOCATION: us-central1-a + K8S_NAMESPACE: default + K8S_SECRET_NAME: app-env + HELM_RELEASE: safe-settings + jobs: - deploy-to-k8s: - if: ${{ github.event.inputs.status == 'passed' }} - name: Deploy to Kubernetes + test: + name: Test runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v6 - - uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - uses: azure/aks-set-context@c7eb093e5a5d47caa333f64974d5fd1cd4bf069d + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: - resource-group: ${{env.AZURE_RESOURCE_GROUP}} - cluster-name: ${{env.AZURE_AKS_CLUSTER}} - id: login - - run: | - kubectl get deployment - - name: app-env - uses: azure/k8s-create-secret@6e0ba8047235646753f2a3a3b359b4d0006ff218 + node-version: 24 + cache: npm + - run: npm ci + - run: npm run test:unit:ci + + deploy: + name: Deploy production + if: ${{ vars.SAFE_SETTINGS_DEPLOY_ENABLED == 'true' }} + needs: test + runs-on: ubuntu-latest + environment: production + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v7 + + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v3 with: - namespace: 'default' - secret-type: 'generic' - arguments: --from-literal=APP_ID=${{ secrets.APP_ID }} --from-literal=PRIVATE_KEY=${{ secrets.PRIVATE_KEY }} --from-literal=WEBHOOK_SECRET=${{ secrets.WEBHOOK_SECRET }} - secret-name: app-env - - name: Set imagePullSecret - uses: azure/k8s-create-secret@6e0ba8047235646753f2a3a3b359b4d0006ff218 + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v3 + + - name: Configure Docker for Artifact Registry + run: gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push image + uses: docker/build-push-action@v7 with: - namespace: ${{env.AZURE_AKS_NAMESPACE}} - container-registry-url: ${{env.IMAGE_REGISTRY_URL}} - container-registry-username: ${{ secrets.DOCKER_USERNAME }} - container-registry-password: ${{ secrets.DOCKER_PASSWORD }} - secret-name: 'image-pull-secret' - id: create-secret - - uses: Azure/k8s-deploy@v5 + context: . + push: true + tags: ${{ env.GCP_REGION }}-docker.pkg.dev/${{ env.GCP_PROJECT_ID }}/${{ env.ARTIFACT_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max,ignore-error=true + + - name: Authenticate to GKE + uses: google-github-actions/get-gke-credentials@v3 with: - namespace: ${{env.AZURE_AKS_NAMESPACE}} - manifests: | - safe-settings.yaml - svc-safe-settings.yaml - #images: ${{env.IMAGE_REGISTRY_URL}}/${{ github.repository }}/${{env.DOCKER_IMAGE_NAME}}:${{ github.sha }} - images: ${{env.IMAGE_REGISTRY_URL}}/yadhav/safe-settings:${{ github.event.inputs.release }} - imagepullsecrets: | - image-pull-secret - kubectl-version: 'latest' + cluster_name: ${{ env.GKE_CLUSTER }} + location: ${{ env.GKE_LOCATION }} + + - name: Verify runtime secret exists + run: kubectl --namespace "${K8S_NAMESPACE}" get secret "${K8S_SECRET_NAME}" >/dev/null + + - name: Deploy with Helm + run: | + helm upgrade --install "${HELM_RELEASE}" ./helm/safe-settings \ + --namespace "${K8S_NAMESPACE}" \ + --values helm/safe-settings/values-hacktron.yaml \ + --set-string image.tag="${GITHUB_SHA}" \ + --atomic \ + --wait \ + --timeout 10m \ + --history-max 10 + + - name: Verify rollout + run: kubectl --namespace "${K8S_NAMESPACE}" rollout status deployment/"${HELM_RELEASE}" --timeout=5m + + - name: Record deployment + run: | + { + echo "### safe-settings deployed" + echo "- Image: \`${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${ARTIFACT_REPOSITORY}/${IMAGE_NAME}:${GITHUB_SHA}\`" + echo "- Cluster: \`${GKE_CLUSTER}\` (\`${GKE_LOCATION}\`)" + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/DEPLOY.md b/DEPLOY.md index cbc65204f..43413938d 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,60 +1,98 @@ # Deployment -This is our own documentation for deploying to GCP. In general this should only need to be done once, unless we need to modify any of the core logic. Changes to settings in [`.github`](./.github) will be automatically picked up. +Safe Settings is deployed to the existing GKE target by +`.github/workflows/deploy-k8s.yml`. A push to `main-enterprise` automatically +deploys application, image, chart, or workflow changes after unit tests pass. +Changes limited to `.github/repos/*.yml` do not rebuild the application. -Authenticate: +Deployment is deliberately disabled until the repository variable +`SAFE_SETTINGS_DEPLOY_ENABLED` is set to `true`. -```bash -$ gcloud auth print-access-token | helm registry login -u oauth2accesstoken \ ---password-stdin us-central1-docker.pkg.dev -``` +## One-time GitHub Actions setup -Package Helm chart: +Create a `production` environment and add these environment secrets: -```bash -$ helm package helm/safe-settings -Successfully packaged chart and saved it to: /home/zeyu/OneDrive/Documents/Projects/hacktron/safe-settings/safe-settings-0.1.0.tgz -``` +- `GCP_WORKLOAD_IDENTITY_PROVIDER` +- `GCP_SERVICE_ACCOUNT` -Push image to GCP artifact repository: +The Workload Identity provider must trust the GitHub OIDC subject +`repo:HacktronAI/safe-settings:environment:production`. The service account +needs permission to push to Artifact Registry and deploy Kubernetes resources: -```bash -$ gcloud artifacts repositories create safe-settings \ - --repository-format=docker \ - --location=us-central1 \ - --description="GitHub policy-as-code" -Create request issued for: [safe-settings] -Waiting for operation [projects/hacktron-462816/locations/us-central1/operations/b15dad24-1d36-409d-8d81-64f983554982] to complete...done. -Created repository [safe-settings]. +- `roles/artifactregistry.writer` +- `roles/container.developer` +- `roles/iam.workloadIdentityUser` granted to the GitHub OIDC principal on the + service account -$ docker tag safe-settings us-central1-docker.pkg.dev/hacktron-462816/safe-settings/github-app:0.1.0 +If the cluster uses additional Kubernetes RBAC, bind the service account to a +role that can manage this release's Deployment, Service, ConfigMap, +ServiceAccount, and related Helm objects in the `default` namespace. -$ docker push us-central1-docker.pkg.dev/hacktron-462816/safe-settings/github-app:0.1.0 -``` +The workflow currently targets: + +- project: `hacktron-462816` +- registry: `us-central1-docker.pkg.dev/hacktron-462816/safe-settings` +- cluster: `safe-settings-cluster` +- cluster location: `us-central1-a` +- Helm release and deployment: `safe-settings` +- namespace: `default` -Push Helm chart: +Only enable deployment after those resources and permissions are confirmed: ```bash -$ helm push safe-settings-0.1.0.tgz oci://us-central1-docker.pkg.dev/hacktron-462816/safe-settings -Pushed: us-central1-docker.pkg.dev/hacktron-462816/safe-settings/safe-settings:0.1.0 -Digest: sha256:818ad22a4f3fec92dbfcf0d244e35024cc25db35a88584742b92a31c00c37656 +gh variable set SAFE_SETTINGS_DEPLOY_ENABLED \ + --repo HacktronAI/safe-settings \ + --body true ``` -Deploy Helm chart: - -```bash -$ gcloud container clusters create --zone us-central1-a safe-settings-cluster -$ gcloud container clusters get-credentials --zone us-central1-a safe-settings-cluster +## One-time runtime secret setup -$ source .env -helm install safe-settings oci://us-central1-docker.pkg.dev/hacktron-462816/safe-settings/safe-settings --version 0.1.0 --set env.ADMIN_REPO="$ADMIN_REPO" --set env.GH_ORG="$GH_ORG" --set env.CRON="$CRON" --set env.APP_ID="\"$APP_ID\"" --set env.PRIVATE_KEY="$PRIVATE_KEY" --set env.WEBHOOK_SECRET="$WEBHOOK_SECRET" --set env.GITHUB_CLIENT_ID="$GITHUB_CLIENT_ID" --set env.GITHUB_CLIENT_SECRET="$GITHUB_CLIENT_SECRET" --set env.WEBHOOK_PROXY_URL="$WEBHOOK_PROXY_URL" +The workflow never copies GitHub App credentials into an image or command +line. The pod reads them from the existing `default/app-env` Kubernetes Secret. +The required keys are: -$ helm upgrade --install safe-settings oci://us-central1-docker.pkg.dev/hacktron-462816/safe-settings/safe-settings --set env.ADMIN_REPO="$ADMIN_REPO" --set env.GH_ORG="$GH_ORG" --set env.CRON="$CRON" --set env.APP_ID="$APP_ID" --set env.PRIVATE_KEY="$PRIVATE_KEY" --set env.WEBHOOK_SECRET="$WEBHOOK_SECRET" --set env.GITHUB_CLIENT_ID="$GITHUB_CLIENT_ID" --set env.GITHUB_CLIENT_SECRET="$GITHUB_CLIENT_SECRET" --set env.WEBHOOK_PROXY_URL="$WEBHOOK_PROXY_URL" --set env.LOG_LEVEL="$LOG_LEVEL" --force -``` +- `APP_ID` +- `PRIVATE_KEY` +- `WEBHOOK_SECRET` +- `WEBHOOK_PROXY_URL` while Smee is used -If we need to SSH into the nodes through `gcloud computer ssh --tunnel-through-iap`: +After authenticating to GCP and selecting the cluster, populate the Secret from +the ignored local `.env` file: ```bash -gcloud container node-pools update --zone us-central1-a default-pool --cluster=safe-settings-cluster --tags=ssh-iap -gcloud container clusters update --zone us-central1-a safe-settings-cluster --autoprovisioning-network-tags=ssh-iap +gcloud auth login +gcloud container clusters get-credentials safe-settings-cluster \ + --project hacktron-462816 \ + --zone us-central1-a +./script/bootstrap-k8s-secret ``` + +The bootstrap script writes values only to a private temporary directory, +applies the Secret, and removes the temporary files without printing values. + +## Webhooks + +The GitHub App must have an active runtime and a webhook transport. The current +App configuration points to Smee. Keeping `WEBHOOK_PROXY_URL` in `app-env` +causes Probot to connect outbound to that Smee channel; a public Kubernetes +Ingress is not required for this initial setup. The webhook endpoint inside the +application is `/api/github/webhooks`. + +For a direct production webhook later: + +1. Provide a DNS hostname and HTTPS certificate. +2. Enable the Helm ingress for that hostname. +3. Change the GitHub App webhook URL to + `https:///api/github/webhooks` with SSL verification enabled. +4. Remove `WEBHOOK_PROXY_URL` from `app-env` and restart the deployment. +5. Send a test delivery and confirm a `2xx` response plus application logs for + that delivery. + +Smee returning `200` only confirms that Smee accepted a GitHub delivery; it +does not prove a Safe Settings pod was connected to consume it. + +## Manual deployment + +The Actions workflow can also be started from **Actions → Deploy +safe-settings → Run workflow**. It uses the same tests, immutable image tag, +runtime Secret check, and atomic Helm deployment as automatic pushes. diff --git a/helm/safe-settings/values-hacktron.yaml b/helm/safe-settings/values-hacktron.yaml new file mode 100644 index 000000000..3a30c1ae1 --- /dev/null +++ b/helm/safe-settings/values-hacktron.yaml @@ -0,0 +1,24 @@ +replicaCount: 1 + +image: + repository: us-central1-docker.pkg.dev/hacktron-462816/safe-settings/github-app + pullPolicy: IfNotPresent + +envFrom: + - secretRef: + name: app-env + +env: + ADMIN_REPO: safe-settings + GH_ORG: HacktronAI + CRON: "0 * * * *" + LOG_LEVEL: info + +service: + type: ClusterIP + port: 3000 + +# The GitHub App currently delivers to Smee. WEBHOOK_PROXY_URL belongs in the +# app-env Secret so the pod connects to that channel without a public ingress. +ingress: + enabled: false diff --git a/script/bootstrap-k8s-secret b/script/bootstrap-k8s-secret new file mode 100755 index 000000000..e0a2a9459 --- /dev/null +++ b/script/bootstrap-k8s-secret @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +env_file="${1:-.env}" +namespace="${K8S_NAMESPACE:-default}" +secret_name="${K8S_SECRET_NAME:-app-env}" + +if [[ ! -f "${env_file}" ]]; then + echo "Environment file not found: ${env_file}" >&2 + exit 1 +fi + +set -a +# shellcheck disable=SC1090 +source "${env_file}" +set +a + +for name in APP_ID PRIVATE_KEY WEBHOOK_SECRET; do + if [[ -z "${!name:-}" ]]; then + echo "${name} is required in ${env_file}" >&2 + exit 1 + fi +done + +secret_dir="$(mktemp -d)" +trap 'rm -rf "${secret_dir}"' EXIT +umask 077 + +printf '%s' "${APP_ID}" > "${secret_dir}/APP_ID" +printf '%s' "${PRIVATE_KEY}" > "${secret_dir}/PRIVATE_KEY" +printf '%s' "${WEBHOOK_SECRET}" > "${secret_dir}/WEBHOOK_SECRET" + +secret_args=( + --from-file="APP_ID=${secret_dir}/APP_ID" + --from-file="PRIVATE_KEY=${secret_dir}/PRIVATE_KEY" + --from-file="WEBHOOK_SECRET=${secret_dir}/WEBHOOK_SECRET" +) + +if [[ -n "${WEBHOOK_PROXY_URL:-}" ]]; then + printf '%s' "${WEBHOOK_PROXY_URL}" > "${secret_dir}/WEBHOOK_PROXY_URL" + secret_args+=(--from-file="WEBHOOK_PROXY_URL=${secret_dir}/WEBHOOK_PROXY_URL") +fi + +kubectl create secret generic "${secret_name}" \ + --namespace "${namespace}" \ + "${secret_args[@]}" \ + --dry-run=client \ + --output yaml | kubectl apply --filename - + +echo "Updated ${namespace}/${secret_name} without printing secret values." From 3a9aab42306570420f4f6ad428ae9943f6231c72 Mon Sep 17 00:00:00 2001 From: "Zeyu (Zayne) Zhang" Date: Sat, 8 Aug 2026 17:32:43 +0800 Subject: [PATCH 2/4] docs: clarify deployment secret scope --- DEPLOY.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DEPLOY.md b/DEPLOY.md index 43413938d..8bfb5004f 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -10,7 +10,9 @@ Deployment is deliberately disabled until the repository variable ## One-time GitHub Actions setup -Create a `production` environment and add these environment secrets: +Create a `production` environment and make these secrets available to the +repository, either by sharing the existing Hacktron organization secrets or by +adding repository/environment secrets with the same names: - `GCP_WORKLOAD_IDENTITY_PROVIDER` - `GCP_SERVICE_ACCOUNT` From f860f9b9896f471296a0be854c67aa40459eba4b Mon Sep 17 00:00:00 2001 From: "Zeyu (Zayne) Zhang" Date: Sat, 8 Aug 2026 17:37:34 +0800 Subject: [PATCH 3/4] fix: protect webhook proxy configuration --- DEPLOY.md | 4 +++- script/bootstrap-k8s-secret | 39 +++++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 8bfb5004f..76e688035 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -86,7 +86,9 @@ For a direct production webhook later: 2. Enable the Helm ingress for that hostname. 3. Change the GitHub App webhook URL to `https:///api/github/webhooks` with SSL verification enabled. -4. Remove `WEBHOOK_PROXY_URL` from `app-env` and restart the deployment. +4. Remove `WEBHOOK_PROXY_URL` from `.env` and run + `DIRECT_WEBHOOK=true ./script/bootstrap-k8s-secret`; the explicit flag + removes the proxy key from `app-env`. 5. Send a test delivery and confirm a `2xx` response plus application logs for that delivery. diff --git a/script/bootstrap-k8s-secret b/script/bootstrap-k8s-secret index e0a2a9459..1a949e6e4 100755 --- a/script/bootstrap-k8s-secret +++ b/script/bootstrap-k8s-secret @@ -26,6 +26,16 @@ secret_dir="$(mktemp -d)" trap 'rm -rf "${secret_dir}"' EXIT umask 077 +direct_webhook="${DIRECT_WEBHOOK:-false}" +if [[ "${direct_webhook}" != "true" && "${direct_webhook}" != "false" ]]; then + echo "DIRECT_WEBHOOK must be true or false" >&2 + exit 1 +fi +if [[ "${direct_webhook}" == "true" && -n "${WEBHOOK_PROXY_URL:-}" ]]; then + echo "Unset WEBHOOK_PROXY_URL when DIRECT_WEBHOOK=true" >&2 + exit 1 +fi + printf '%s' "${APP_ID}" > "${secret_dir}/APP_ID" printf '%s' "${PRIVATE_KEY}" > "${secret_dir}/PRIVATE_KEY" printf '%s' "${WEBHOOK_SECRET}" > "${secret_dir}/WEBHOOK_SECRET" @@ -36,8 +46,25 @@ secret_args=( --from-file="WEBHOOK_SECRET=${secret_dir}/WEBHOOK_SECRET" ) -if [[ -n "${WEBHOOK_PROXY_URL:-}" ]]; then - printf '%s' "${WEBHOOK_PROXY_URL}" > "${secret_dir}/WEBHOOK_PROXY_URL" +webhook_proxy_url="${WEBHOOK_PROXY_URL:-}" +if [[ "${direct_webhook}" == "false" && -z "${webhook_proxy_url}" ]]; then + encoded_webhook_proxy_url="$( + kubectl get secret "${secret_name}" \ + --namespace "${namespace}" \ + --output 'jsonpath={.data.WEBHOOK_PROXY_URL}' 2>/dev/null || true + )" + if [[ -n "${encoded_webhook_proxy_url}" ]]; then + webhook_proxy_url="$(printf '%s' "${encoded_webhook_proxy_url}" | base64 --decode)" + fi +fi + +if [[ "${direct_webhook}" == "false" && -z "${webhook_proxy_url}" ]]; then + echo "WEBHOOK_PROXY_URL is required for Smee; set DIRECT_WEBHOOK=true only for a configured direct HTTPS webhook" >&2 + exit 1 +fi + +if [[ "${direct_webhook}" == "false" ]]; then + printf '%s' "${webhook_proxy_url}" > "${secret_dir}/WEBHOOK_PROXY_URL" secret_args+=(--from-file="WEBHOOK_PROXY_URL=${secret_dir}/WEBHOOK_PROXY_URL") fi @@ -47,4 +74,12 @@ kubectl create secret generic "${secret_name}" \ --dry-run=client \ --output yaml | kubectl apply --filename - +if [[ "${direct_webhook}" == "true" ]] && \ + [[ -n "$(kubectl get secret "${secret_name}" --namespace "${namespace}" --output 'jsonpath={.data.WEBHOOK_PROXY_URL}')" ]]; then + kubectl patch secret "${secret_name}" \ + --namespace "${namespace}" \ + --type json \ + --patch '[{"op":"remove","path":"/data/WEBHOOK_PROXY_URL"}]' >/dev/null +fi + echo "Updated ${namespace}/${secret_name} without printing secret values." From 821519221f85affcb2f714f9c7eb2eaffc7e9ce1 Mon Sep 17 00:00:00 2001 From: "Zeyu (Zayne) Zhang" Date: Sat, 8 Aug 2026 18:08:26 +0800 Subject: [PATCH 4/4] Harden safe-settings deployment identity --- .github/workflows/deploy-k8s.yml | 7 +--- DEPLOY.md | 54 +++++++++++++++++++++------ helm/safe-settings/deployer-rbac.yaml | 44 ++++++++++++++++++++++ helm/safe-settings/namespace.yaml | 4 ++ 4 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 helm/safe-settings/deployer-rbac.yaml create mode 100644 helm/safe-settings/namespace.yaml diff --git a/.github/workflows/deploy-k8s.yml b/.github/workflows/deploy-k8s.yml index 0d91a2f27..04f2ca072 100644 --- a/.github/workflows/deploy-k8s.yml +++ b/.github/workflows/deploy-k8s.yml @@ -28,9 +28,9 @@ env: IMAGE_NAME: github-app GKE_CLUSTER: safe-settings-cluster GKE_LOCATION: us-central1-a - K8S_NAMESPACE: default - K8S_SECRET_NAME: app-env + K8S_NAMESPACE: safe-settings HELM_RELEASE: safe-settings + HELM_DRIVER: configmap jobs: test: @@ -88,9 +88,6 @@ jobs: cluster_name: ${{ env.GKE_CLUSTER }} location: ${{ env.GKE_LOCATION }} - - name: Verify runtime secret exists - run: kubectl --namespace "${K8S_NAMESPACE}" get secret "${K8S_SECRET_NAME}" >/dev/null - - name: Deploy with Helm run: | helm upgrade --install "${HELM_RELEASE}" ./helm/safe-settings \ diff --git a/DEPLOY.md b/DEPLOY.md index 76e688035..f17b5df5c 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -17,18 +17,29 @@ adding repository/environment secrets with the same names: - `GCP_WORKLOAD_IDENTITY_PROVIDER` - `GCP_SERVICE_ACCOUNT` -The Workload Identity provider must trust the GitHub OIDC subject -`repo:HacktronAI/safe-settings:environment:production`. The service account -needs permission to push to Artifact Registry and deploy Kubernetes resources: - -- `roles/artifactregistry.writer` -- `roles/container.developer` +The Workload Identity provider must map `attribute.repository` and trust +`HacktronAI/safe-settings`. Use the dedicated service account +`safe-settings-deployer@hacktron-462816.iam.gserviceaccount.com`; do not reuse a +broad infrastructure runner. It needs: + +- `roles/artifactregistry.writer` on only the `safe-settings` Artifact Registry + repository +- `roles/container.clusterViewer` on project `hacktron-462816` - `roles/iam.workloadIdentityUser` granted to the GitHub OIDC principal on the service account -If the cluster uses additional Kubernetes RBAC, bind the service account to a -role that can manage this release's Deployment, Service, ConfigMap, -ServiceAccount, and related Helm objects in the `default` namespace. +Create the dedicated namespace and apply its scoped Kubernetes role once from +an administrator context: + +```bash +kubectl apply --filename helm/safe-settings/namespace.yaml +kubectl apply --filename helm/safe-settings/deployer-rbac.yaml +``` + +This limits the CI identity to the Helm resources used by this chart in the +`safe-settings` namespace instead of granting project-wide Kubernetes +developer access. Helm stores its release metadata in ConfigMaps so CI does not +need permission to read Kubernetes Secrets. The workflow currently targets: @@ -37,7 +48,7 @@ The workflow currently targets: - cluster: `safe-settings-cluster` - cluster location: `us-central1-a` - Helm release and deployment: `safe-settings` -- namespace: `default` +- namespace: `safe-settings` Only enable deployment after those resources and permissions are confirmed: @@ -50,7 +61,7 @@ gh variable set SAFE_SETTINGS_DEPLOY_ENABLED \ ## One-time runtime secret setup The workflow never copies GitHub App credentials into an image or command -line. The pod reads them from the existing `default/app-env` Kubernetes Secret. +line. The pod reads them from the `safe-settings/app-env` Kubernetes Secret. The required keys are: - `APP_ID` @@ -66,7 +77,8 @@ gcloud auth login gcloud container clusters get-credentials safe-settings-cluster \ --project hacktron-462816 \ --zone us-central1-a -./script/bootstrap-k8s-secret +kubectl apply --filename helm/safe-settings/namespace.yaml +K8S_NAMESPACE=safe-settings ./script/bootstrap-k8s-secret ``` The bootstrap script writes values only to a private temporary directory, @@ -95,6 +107,24 @@ For a direct production webhook later: Smee returning `200` only confirms that Smee accepted a GitHub delivery; it does not prove a Safe Settings pod was connected to consume it. +## First deployment cutover + +The legacy release runs as `default/safe-settings`. Do not run both releases +against the same Smee channel because a delivery could be processed twice. +After this workflow is merged, perform the first deployment as a short, +reversible cutover: + +1. Keep `SAFE_SETTINGS_DEPLOY_ENABLED=false` while merging the workflow. +2. Scale `default/safe-settings` to zero replicas. +3. Set `SAFE_SETTINGS_DEPLOY_ENABLED=true` and manually run the deployment + workflow from `main-enterprise`. +4. Verify `safe-settings/safe-settings` is ready and its logs show the Smee + client receiving a delivery. +5. If verification fails, set the variable back to `false` and scale the + legacy deployment back to one replica. + +After a successful cutover, pushes to `main-enterprise` deploy automatically. + ## Manual deployment The Actions workflow can also be started from **Actions → Deploy diff --git a/helm/safe-settings/deployer-rbac.yaml b/helm/safe-settings/deployer-rbac.yaml new file mode 100644 index 000000000..57ecde4ee --- /dev/null +++ b/helm/safe-settings/deployer-rbac.yaml @@ -0,0 +1,44 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: safe-settings-deployer + namespace: safe-settings +rules: + - apiGroups: [""] + resources: + - configmaps + - serviceaccounts + - services + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] + - apiGroups: [""] + resources: + - endpoints + - pods + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: + - deployments + - replicasets + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] + - apiGroups: ["autoscaling"] + resources: + - horizontalpodautoscalers + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] + - apiGroups: ["networking.k8s.io"] + resources: + - ingresses + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: safe-settings-deployer + namespace: safe-settings +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: safe-settings-deployer +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: safe-settings-deployer@hacktron-462816.iam.gserviceaccount.com diff --git a/helm/safe-settings/namespace.yaml b/helm/safe-settings/namespace.yaml new file mode 100644 index 000000000..7e06370ad --- /dev/null +++ b/helm/safe-settings/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: safe-settings