From 37f69c053d30ba9368428b49b7c38a54a580e2cc Mon Sep 17 00:00:00 2001 From: maphew <486200+maphew@users.noreply.github.com> Date: Mon, 28 Sep 2026 05:49:21 +0000 Subject: [PATCH 1/8] ci: guard privileged workflows so they cannot run in forks Add a job-level guard to fork-triggerable privileged workflows so forks with Actions enabled cannot trigger deploys, releases, migrations, or container pushes. Guarded workflows: bump-openclaw, check-md-table-padding, codeql, deploy-production, deploy-staging, deploy-workers, deploy-kiloclaw, extension-store-release, kilo-app-release, redeploy-web, auto-docs. Closes #6248 Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .github/workflows/auto-docs.yml | 4 + .github/workflows/bump-openclaw.yml | 5 ++ .github/workflows/check-md-table-padding.yml | 5 ++ .github/workflows/codeql.yml | 4 + .github/workflows/deploy-kiloclaw.yml | 7 +- .github/workflows/deploy-production.yml | 73 +++++-------------- .github/workflows/deploy-staging.yml | 24 +++--- .github/workflows/deploy-workers.yml | 7 +- .github/workflows/extension-store-release.yml | 6 +- .github/workflows/kilo-app-release.yml | 11 +-- .github/workflows/redeploy-web.yml | 5 ++ 11 files changed, 71 insertions(+), 80 deletions(-) diff --git a/.github/workflows/auto-docs.yml b/.github/workflows/auto-docs.yml index 2046bc5f6d2..e3ef77dedf9 100644 --- a/.github/workflows/auto-docs.yml +++ b/.github/workflows/auto-docs.yml @@ -25,6 +25,10 @@ concurrency: jobs: call-valtown: + # Guard: this workflow forwards merged PR metadata to an external webhook + # and only makes sense for the canonical repo. A fork with Actions enabled + # would otherwise attempt the same job and fail on missing repo secrets. + if: github.repository == 'Kilo-Org/cloud' runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.github/workflows/bump-openclaw.yml b/.github/workflows/bump-openclaw.yml index 65f719c8f9a..a1f92bc2892 100644 --- a/.github/workflows/bump-openclaw.yml +++ b/.github/workflows/bump-openclaw.yml @@ -10,6 +10,11 @@ permissions: jobs: check: + # Guard: this workflow acts on shared state (the OpenClaw release webhook, + # which edits the KiloClaw Dockerfile and opens a bump PR) and only makes + # sense for the canonical repo. A fork with Actions enabled would otherwise + # attempt the same job and fail on missing repo secrets/vars. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 10 steps: diff --git a/.github/workflows/check-md-table-padding.yml b/.github/workflows/check-md-table-padding.yml index 54165d5bc93..4c0cf116d2c 100644 --- a/.github/workflows/check-md-table-padding.yml +++ b/.github/workflows/check-md-table-padding.yml @@ -9,6 +9,11 @@ on: jobs: check: name: Check markdown table padding + # Guard: a manual dispatch from a fork would otherwise run this check + # against the fork's own files. For pull_request events github.repository is + # already the base repo, so the guard is a no-op there and only blocks the + # fork-triggerable path. + if: github.repository == 'Kilo-Org/cloud' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ebb0a840f05..9ac4c9b7103 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,6 +15,10 @@ permissions: jobs: analyze: name: Analyze (${{ matrix.language }}) + # Guard: a fork with Actions enabled can trigger this workflow_dispatch, + # and CodeQL would run against the fork's own code with the fork's + # security-events scope. Only the canonical repo needs this analysis. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 30 strategy: diff --git a/.github/workflows/deploy-kiloclaw.yml b/.github/workflows/deploy-kiloclaw.yml index 75bd385f775..9731cb8981d 100644 --- a/.github/workflows/deploy-kiloclaw.yml +++ b/.github/workflows/deploy-kiloclaw.yml @@ -25,6 +25,10 @@ permissions: jobs: deploy: + # Guard: this workflow_dispatch is reachable from a fork, and it builds and + # pushes a container image to Fly and GHCR using repo secrets. Only the + # canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 30 name: Deploy KiloClaw @@ -266,7 +270,7 @@ jobs: # when its content was last built, so we don't pull a large unchanged image. attest-sbom: needs: deploy - if: needs.deploy.outputs.image-built == 'true' && needs.deploy.outputs.image-digest != '' + if: needs.deploy.outputs.image-built == 'true' && needs.deploy.outputs.image-digest != '' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 name: Attest image SBOM @@ -306,6 +310,7 @@ jobs: # continue-on-error ensures a dev push failure never blocks prod. push-dev-image: needs: [deploy] + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 10 name: Push Dev Image diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 5ecf8480284..6fa3c75c736 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -26,7 +26,8 @@ jobs: check-production-db-startup: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 environment: production @@ -59,7 +60,8 @@ jobs: run-migrations: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 environment: production @@ -92,7 +94,8 @@ jobs: stage-app: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: production @@ -103,7 +106,8 @@ jobs: stage-global-app: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: production @@ -125,6 +129,7 @@ jobs: promote-app: needs: [stage-app, check-production-db-startup, run-migrations] + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/promote-vercel-deployment.yml with: deployment_url: ${{ needs.stage-app.outputs.deployment_url }} @@ -135,25 +140,16 @@ jobs: promote-global-app: needs: [stage-global-app, check-production-db-startup, run-migrations] + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/promote-vercel-deployment.yml with: deployment_url: ${{ needs.stage-global-app.outputs.deployment_url }} secrets: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - promote-ai-gateway: needs: [stage-ai-gateway, check-production-db-startup, run-migrations] - uses: ./.github/workflows/promote-vercel-deployment.yml - with: - deployment_url: ${{ needs.stage-ai-gateway.outputs.deployment_url }} - # The gateway has no dedicated Axiom dataset. Its logs reach Axiom through - # the Axiom Vercel integration's drain, which feeds the shared `vercel` - # dataset, so annotate promotes there. - axiom_annotation_dataset: vercel - axiom_expected_project: kilocode-ai-gateway - secrets: - AXIOM_ANNOTATION_TOKEN: ${{ secrets.AXIOM_ANNOTATION_TOKEN }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' deploy-workers: needs: @@ -170,6 +166,7 @@ jobs: ] if: >- ${{ + github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.check-changes.outputs.should_deploy == 'true' && needs.check-production-db-startup.result == 'success' && @@ -205,7 +202,8 @@ jobs: detect-gastown-wasteland-changes: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 outputs: @@ -280,6 +278,7 @@ jobs: ] if: >- ${{ + github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.check-changes.outputs.should_deploy == 'true' && needs.check-production-db-startup.result == 'success' && @@ -339,44 +338,8 @@ jobs: continue-on-error: true run: pnpm --filter cloudflare-gastown sentry:sourcemaps - # The KiloClaw image and worker deploy (deploy-kiloclaw.yml) is not part of - # the scheduled pipeline. It runs manually via workflow_dispatch from the - # Actions tab. - - record-deployment: - needs: - [ - check-changes, - check-production-db-startup, - run-migrations, - stage-app, - stage-global-app, - stage-ai-gateway, - promote-app, - promote-global-app, - promote-ai-gateway, - deploy-workers, - detect-gastown-wasteland-changes, - deploy-gastown-wasteland, - ] - if: >- - ${{ - !cancelled() && - needs.check-changes.outputs.should_deploy == 'true' && - needs.check-production-db-startup.result == 'success' && - needs.run-migrations.result == 'success' && - needs.stage-app.result == 'success' && - needs.stage-global-app.result == 'success' && - needs.stage-ai-gateway.result == 'success' && - needs.promote-app.result == 'success' && - needs.promote-global-app.result == 'success' && - needs.promote-ai-gateway.result == 'success' && - needs.deploy-workers.result == 'success' && - needs.detect-gastown-wasteland-changes.result == 'success' && - contains(fromJSON('["success","skipped"]'), needs.deploy-gastown-wasteland.result) - }} - runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} - timeout-minutes: 5 + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' permissions: deployments: write steps: diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 2a7a3717c62..77192b35c9e 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -22,7 +22,8 @@ jobs: check-staging-db-startup: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 environment: staging @@ -55,7 +56,8 @@ jobs: run-migrations: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 environment: staging @@ -88,7 +90,8 @@ jobs: deploy-app: needs: [check-changes, check-staging-db-startup, run-migrations] - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: staging @@ -99,7 +102,8 @@ jobs: deploy-global-app: needs: [check-changes, check-staging-db-startup, run-migrations] - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: staging @@ -120,16 +124,8 @@ jobs: VERCEL_PROJECT_TOKEN: ${{ secrets.VERCEL_TOKEN_AI_GATEWAY }} deploy-workers: - needs: - [ - check-changes, - check-staging-db-startup, - run-migrations, - deploy-app, - deploy-global-app, - deploy-ai-gateway, - ] - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/deploy-workers.yml with: target_environment: staging diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index 19bfed5891b..81f0c56a71e 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -47,7 +47,7 @@ concurrency: jobs: deploy-manual: - if: inputs.worker != '' + if: inputs.worker != '' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 25 name: Deploy ${{ inputs.worker }} @@ -83,7 +83,8 @@ jobs: command: ${{ inputs.target_environment == 'production' && 'deploy' || format('deploy --env {0}', inputs.target_environment) }} detect-changes: - if: inputs.worker == '' && inputs.base_sha != '' + # Guard: privileged workflow — only the canonical repo may run it. + if: inputs.worker == '' && (inputs.base_sha != '' || inputs.target_environment != 'production') && github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 outputs: @@ -214,7 +215,7 @@ jobs: deploy-changed: needs: detect-changes - if: needs.detect-changes.outputs.matrix != '[]' && needs.detect-changes.outputs.matrix != '' + if: needs.detect-changes.outputs.matrix != '[]' && needs.detect-changes.outputs.matrix != '' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 25 environment: ${{ inputs.target_environment }} diff --git a/.github/workflows/extension-store-release.yml b/.github/workflows/extension-store-release.yml index a47cdf32599..251c00bce90 100644 --- a/.github/workflows/extension-store-release.yml +++ b/.github/workflows/extension-store-release.yml @@ -18,6 +18,7 @@ permissions: jobs: build-artifacts: + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 30 env: @@ -86,8 +87,9 @@ jobs: bump-version: needs: build-artifacts # Only a main dispatch bumps: a branch dispatch would open a PR carrying - # unrelated commits. - if: github.ref == 'refs/heads/main' + # unrelated commits. Also guarded to the canonical repo: a fork dispatch + # would otherwise push a bump branch and open a PR against the fork. + if: github.ref == 'refs/heads/main' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 10 permissions: diff --git a/.github/workflows/kilo-app-release.yml b/.github/workflows/kilo-app-release.yml index 59c4170841e..4414e71582d 100644 --- a/.github/workflows/kilo-app-release.yml +++ b/.github/workflows/kilo-app-release.yml @@ -34,6 +34,7 @@ permissions: jobs: check-changes: + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 permissions: @@ -153,12 +154,12 @@ jobs: validate: needs: [check-changes] - if: needs.check-changes.outputs.should_build == 'true' && needs.check-changes.outputs.allowed == 'true' + if: needs.check-changes.outputs.should_build == 'true' && needs.check-changes.outputs.allowed == 'true' && github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/kilo-app-ci.yml preflight: needs: [check-changes] - if: needs.check-changes.outputs.should_build == 'true' && github.ref == 'refs/heads/main' && needs.check-changes.outputs.allowed == 'true' + if: needs.check-changes.outputs.should_build == 'true' && github.ref == 'refs/heads/main' && needs.check-changes.outputs.allowed == 'true' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 permissions: @@ -223,7 +224,7 @@ jobs: build-and-submit: needs: [check-changes, validate, preflight] - if: needs.check-changes.outputs.should_build == 'true' && github.ref == 'refs/heads/main' && needs.check-changes.outputs.allowed == 'true' + if: needs.check-changes.outputs.should_build == 'true' && github.ref == 'refs/heads/main' && needs.check-changes.outputs.allowed == 'true' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 60 outputs: @@ -460,7 +461,7 @@ jobs: # the section lands, which loses the build's changelog line for ever. bump-version: needs: [check-changes] - if: needs.check-changes.outputs.should_build == 'true' && github.ref == 'refs/heads/main' && needs.check-changes.outputs.allowed == 'true' + if: needs.check-changes.outputs.should_build == 'true' && github.ref == 'refs/heads/main' && needs.check-changes.outputs.allowed == 'true' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 10 permissions: @@ -546,7 +547,7 @@ jobs: # build writes it above its own section. changelog: needs: [check-changes, bump-version, build-and-submit] - if: needs.check-changes.outputs.should_build == 'true' && needs.check-changes.outputs.allowed == 'true' && github.ref == 'refs/heads/main' + if: needs.check-changes.outputs.should_build == 'true' && needs.check-changes.outputs.allowed == 'true' && github.ref == 'refs/heads/main' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 10 permissions: diff --git a/.github/workflows/redeploy-web.yml b/.github/workflows/redeploy-web.yml index 828b4a7a02c..fd9587f8f69 100644 --- a/.github/workflows/redeploy-web.yml +++ b/.github/workflows/redeploy-web.yml @@ -12,6 +12,7 @@ concurrency: jobs: verify-main: + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 @@ -25,6 +26,7 @@ jobs: stage-app: needs: verify-main + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: production @@ -34,6 +36,7 @@ jobs: stage-global-app: needs: verify-main + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: production @@ -43,6 +46,7 @@ jobs: promote-app: needs: stage-app + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/promote-vercel-deployment.yml with: deployment_url: ${{ needs.stage-app.outputs.deployment_url }} @@ -53,6 +57,7 @@ jobs: promote-global-app: needs: stage-global-app + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/promote-vercel-deployment.yml with: deployment_url: ${{ needs.stage-global-app.outputs.deployment_url }} From daf71c22e0b9a9385e1f06ed37ff57fe6ead5b8a Mon Sep 17 00:00:00 2001 From: maphew <486200+maphew@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:13:05 +0000 Subject: [PATCH 2/8] ci: guard privileged workflows so they cannot run in forks Complete the issue #6248 scope: add the canonical-repo guard to the remaining guarded workflows (push-dev-kiloclaw, sbom, setup-smoke, stale-prs, promote-vercel-deployment, stage-vercel-deployment) and add consistency comments to the bare guards in deploy-production, deploy-staging, deploy-workers, kilo-app-release, and redeploy-web. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .github/workflows/deploy-production.yml | 4 +++- .github/workflows/deploy-staging.yml | 4 +++- .github/workflows/deploy-workers.yml | 4 ++++ .github/workflows/kilo-app-release.yml | 4 ++++ .github/workflows/promote-vercel-deployment.yml | 5 +++++ .github/workflows/push-dev-kiloclaw.yml | 3 +++ .github/workflows/redeploy-web.yml | 4 ++++ .github/workflows/sbom.yml | 4 ++++ .github/workflows/setup-smoke.yml | 4 ++++ .github/workflows/stage-vercel-deployment.yml | 5 +++++ .github/workflows/stale-prs.yaml | 4 ++++ 11 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 6fa3c75c736..e0d4663771a 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -26,7 +26,9 @@ jobs: check-production-db-startup: needs: check-changes - # Guard: privileged workflow — only the canonical repo may run it. + # Guard: this workflow only triggers on pushes to main, so the guard is + # defense-in-depth rather than a fork block. It is applied to every job + # so the workflow cannot run outside the canonical repo under any path. if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 77192b35c9e..22c2da79115 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -22,7 +22,9 @@ jobs: check-staging-db-startup: needs: check-changes - # Guard: privileged workflow — only the canonical repo may run it. + # Guard: this workflow_dispatch is reachable from a fork, and it deploys + # the web app to staging using repo secrets. Only the canonical repo may + # run it. if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index 81f0c56a71e..a0d3ec86664 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -47,6 +47,10 @@ concurrency: jobs: deploy-manual: + # Guard: this workflow_dispatch is reachable from a fork, and it deploys + # Cloudflare Workers using repo secrets. Only the canonical repo may run + # it. (Also called as a reusable workflow from deploy-staging.yml, whose + # caller job is itself guarded.) if: inputs.worker != '' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 25 diff --git a/.github/workflows/kilo-app-release.yml b/.github/workflows/kilo-app-release.yml index 4414e71582d..ee3d78041a0 100644 --- a/.github/workflows/kilo-app-release.yml +++ b/.github/workflows/kilo-app-release.yml @@ -34,6 +34,10 @@ permissions: jobs: check-changes: + # Guard: this workflow_dispatch/schedule is reachable from a fork, and it + # submits a mobile build to the App Store / Google Play using repo secrets. + # Only the canonical repo may run it. (For push: main, github.repository is + # already the canonical repo, so the guard is a no-op there.) if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 diff --git a/.github/workflows/promote-vercel-deployment.yml b/.github/workflows/promote-vercel-deployment.yml index 7fa1115ce28..ae8a7da5425 100644 --- a/.github/workflows/promote-vercel-deployment.yml +++ b/.github/workflows/promote-vercel-deployment.yml @@ -30,6 +30,11 @@ permissions: jobs: promote: + # Guard: this reusable workflow promotes a Vercel deployment to production + # using repo secrets. It is only ever called by guarded caller jobs, but + # the guard is also applied here so the workflow cannot run outside the + # canonical repo even if a caller skips its own guard. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 environment: production diff --git a/.github/workflows/push-dev-kiloclaw.yml b/.github/workflows/push-dev-kiloclaw.yml index 4e6aec116bf..7b5a47868d5 100644 --- a/.github/workflows/push-dev-kiloclaw.yml +++ b/.github/workflows/push-dev-kiloclaw.yml @@ -9,6 +9,9 @@ permissions: jobs: push-dev: + # Guard: this workflow_dispatch builds and pushes a container image to + # Fly and GHCR using repo secrets. Only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 30 name: Build & Push Dev Image diff --git a/.github/workflows/redeploy-web.yml b/.github/workflows/redeploy-web.yml index fd9587f8f69..50423f8932d 100644 --- a/.github/workflows/redeploy-web.yml +++ b/.github/workflows/redeploy-web.yml @@ -12,6 +12,10 @@ concurrency: jobs: verify-main: + # Guard: this workflow_dispatch is reachable from a fork, and it deploys + # the web app to production using repo secrets. Only the canonical repo + # may run it. (verify-main already enforces GITHUB_REF == refs/heads/main, + # so the guard is defense-in-depth for the dispatch path.) if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 84b92eab8d4..f78b0745200 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -22,6 +22,10 @@ permissions: jobs: source-sbom: + # Guard: a fork with Actions enabled can trigger this on schedule or via + # workflow_dispatch. Only the canonical repo needs the weekly SBOM + # regeneration. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 10 name: Source dependency SBOM diff --git a/.github/workflows/setup-smoke.yml b/.github/workflows/setup-smoke.yml index b75f5bcb463..fd004435943 100644 --- a/.github/workflows/setup-smoke.yml +++ b/.github/workflows/setup-smoke.yml @@ -25,6 +25,10 @@ concurrency: jobs: setup-smoke: + # Guard: a fork with Actions enabled can trigger this on schedule or via + # workflow_dispatch, and it posts issue comments via GITHUB_TOKEN. Only + # the canonical repo needs this smoke test. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_LARGE_LABEL || 'ubuntu-24.04-8core' }} timeout-minutes: 45 diff --git a/.github/workflows/stage-vercel-deployment.yml b/.github/workflows/stage-vercel-deployment.yml index ebae24863df..ba002fef0b6 100644 --- a/.github/workflows/stage-vercel-deployment.yml +++ b/.github/workflows/stage-vercel-deployment.yml @@ -30,6 +30,11 @@ permissions: jobs: stage: + # Guard: this reusable workflow stages a Vercel deployment using repo + # secrets. It is only ever called by guarded caller jobs, but the guard is + # also applied here so the workflow cannot run outside the canonical repo + # even if a caller skips its own guard. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 30 environment: ${{ inputs.target_environment }} diff --git a/.github/workflows/stale-prs.yaml b/.github/workflows/stale-prs.yaml index ebb86730fed..813002437c3 100644 --- a/.github/workflows/stale-prs.yaml +++ b/.github/workflows/stale-prs.yaml @@ -6,6 +6,10 @@ on: jobs: stale: + # Guard: a fork with Actions enabled can trigger this on schedule, and it + # marks/closes PRs and posts comments via GITHUB_TOKEN. Only the + # canonical repo needs this automation. + if: github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} permissions: issues: write From bfc5dad68529f22a2f5cc39bbfb78782ef98c7f4 Mon Sep 17 00:00:00 2001 From: maphew <486200+maphew@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:02:53 -0700 Subject: [PATCH 3/8] ci: restore the promote-ai-gateway job lost in the rebase merge --- .github/workflows/deploy-production.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index e0d4663771a..0e71174fb96 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -149,9 +149,21 @@ jobs: secrets: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + promote-ai-gateway: needs: [stage-ai-gateway, check-production-db-startup, run-migrations] # Guard: privileged workflow — only the canonical repo may run it. if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' + uses: ./.github/workflows/promote-vercel-deployment.yml + with: + deployment_url: ${{ needs.stage-ai-gateway.outputs.deployment_url }} + # The gateway has no dedicated Axiom dataset. Its logs reach Axiom through + # the Axiom Vercel integration's drain, which feeds the shared `vercel` + # dataset, so annotate promotes there. + axiom_annotation_dataset: vercel + axiom_expected_project: kilocode-ai-gateway + secrets: + AXIOM_ANNOTATION_TOKEN: ${{ secrets.AXIOM_ANNOTATION_TOKEN }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} deploy-workers: needs: From f036ab52528a105cb079a45fdf1e8febdd4a8f0b Mon Sep 17 00:00:00 2001 From: maphew <486200+maphew@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:04:00 -0700 Subject: [PATCH 4/8] ci: restore the record-deployment job dropped by the rebase merge --- .github/workflows/deploy-production.yml | 41 +++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 0e71174fb96..ee90ac27dde 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -352,8 +352,45 @@ jobs: continue-on-error: true run: pnpm --filter cloudflare-gastown sentry:sourcemaps - # Guard: privileged workflow — only the canonical repo may run it. - if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' + # The KiloClaw image and worker deploy (deploy-kiloclaw.yml) is not part of + # the scheduled pipeline. It runs manually via workflow_dispatch from the + # Actions tab. + + record-deployment: + needs: + [ + check-changes, + check-production-db-startup, + run-migrations, + stage-app, + stage-global-app, + stage-ai-gateway, + promote-app, + promote-global-app, + promote-ai-gateway, + deploy-workers, + detect-gastown-wasteland-changes, + deploy-gastown-wasteland, + ] + if: >- + ${{ + github.repository == 'Kilo-Org/cloud' && + !cancelled() && + needs.check-changes.outputs.should_deploy == 'true' && + needs.check-production-db-startup.result == 'success' && + needs.run-migrations.result == 'success' && + needs.stage-app.result == 'success' && + needs.stage-global-app.result == 'success' && + needs.stage-ai-gateway.result == 'success' && + needs.promote-app.result == 'success' && + needs.promote-global-app.result == 'success' && + needs.promote-ai-gateway.result == 'success' && + needs.deploy-workers.result == 'success' && + needs.detect-gastown-wasteland-changes.result == 'success' && + contains(fromJSON('["success","skipped"]'), needs.deploy-gastown-wasteland.result) + }} + runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} + timeout-minutes: 5 permissions: deployments: write steps: From 2a4a2b9e5136245eb4a321d9513959187902aba2 Mon Sep 17 00:00:00 2001 From: maphew <486200+maphew@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:04:21 -0700 Subject: [PATCH 5/8] ci: guard the staging ai-gateway and record jobs --- .github/workflows/deploy-staging.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 22c2da79115..62e7dcbe1c8 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -116,7 +116,8 @@ jobs: deploy-ai-gateway: needs: [check-changes, check-staging-db-startup, run-migrations] - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: staging @@ -148,6 +149,7 @@ jobs: ] if: >- ${{ + github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.check-changes.outputs.should_deploy == 'true' && needs.check-staging-db-startup.result == 'success' && From d7db102e963e1af42bb519422c5dc582ee468ac0 Mon Sep 17 00:00:00 2001 From: maphew <486200+maphew@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:04:53 -0700 Subject: [PATCH 6/8] ci: restore the staging deploy-workers needs list --- .github/workflows/deploy-staging.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 62e7dcbe1c8..e4e5b3be11d 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -127,6 +127,15 @@ jobs: VERCEL_PROJECT_TOKEN: ${{ secrets.VERCEL_TOKEN_AI_GATEWAY }} deploy-workers: + needs: + [ + check-changes, + check-staging-db-startup, + run-migrations, + deploy-app, + deploy-global-app, + deploy-ai-gateway, + ] # Guard: privileged workflow — only the canonical repo may run it. if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/deploy-workers.yml From beb897ad0e92ed7fbef5ccaacbd525e09f733bcd Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Thu, 22 Oct 2026 16:20:55 -0700 Subject: [PATCH 7/8] ci: fix guards that referenced a missing needs dependency --- .github/workflows/deploy-production.yml | 8 ++++---- .github/workflows/deploy-workers.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index f9424728021..7d787c76b70 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -26,9 +26,9 @@ jobs: check-production-db-startup: needs: check-changes - # Guard: this workflow only triggers on pushes to main, so the guard is - # defense-in-depth rather than a fork block. It is applied to every job - # so the workflow cannot run outside the canonical repo under any path. + # Guard: `schedule` and `workflow_dispatch` are reachable from a fork, so + # this is a real fork block. It is applied to every job so the workflow + # cannot run outside the canonical repo under any path. if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 15 @@ -131,7 +131,7 @@ jobs: promote-ai-gateway: needs: [stage-ai-gateway, check-production-db-startup, run-migrations] # Guard: privileged workflow — only the canonical repo may run it. - if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/promote-vercel-deployment.yml with: deployment_url: ${{ needs.stage-ai-gateway.outputs.deployment_url }} diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index a7ce169bf4b..f88b790de59 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -129,7 +129,7 @@ jobs: detect-changes: # Guard: privileged workflow — only the canonical repo may run it. - if: inputs.worker == '' && (inputs.base_sha != '' || inputs.target_environment != 'production') && github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' + if: inputs.worker == '' && (inputs.base_sha != '' || inputs.target_environment != 'production') && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 outputs: From 9c62fb6ae31986141a0ead8854994af05b4eef8d Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Fri, 23 Oct 2026 10:51:26 -0700 Subject: [PATCH 8/8] ci: close remaining fork-guard gaps in deploy workflows Guard check-changes, stage-ai-gateway, and kilo-mcp-release so the deploy-production comment holds for every job, and restore the base_sha requirement in deploy-workers now that the staging caller passes one. Agent-Signature: kilo-kilo-auto/efficient-unknown-reasoning on behalf of matt wilkie --- .github/workflows/deploy-production.yml | 7 +++++-- .github/workflows/deploy-workers.yml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 7d787c76b70..638341a0066 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -20,6 +20,8 @@ env: jobs: check-changes: + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' uses: ./.github/workflows/check-deployment-changes.yml with: workflow_file: deploy-production.yml @@ -108,7 +110,8 @@ jobs: stage-ai-gateway: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # Guard: privileged workflow — only the canonical repo may run it. + if: github.repository == 'Kilo-Org/cloud' && needs.check-changes.outputs.should_deploy == 'true' uses: ./.github/workflows/stage-vercel-deployment.yml with: target_environment: production @@ -177,7 +180,7 @@ jobs: # successful worker deploy and releases only when the kilo-mcp bundle changed. kilo-mcp-release: needs: [check-changes, deploy-workers] - if: ${{ !cancelled() && needs.deploy-workers.result == 'success' }} + if: ${{ github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.deploy-workers.result == 'success' }} # Ceiling for the reusable workflow's GITHUB_TOKEN (a called workflow cannot # exceed the caller). permissions: diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index 701d31ee346..21b6786a5ee 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -153,7 +153,7 @@ jobs: detect-changes: # Guard: privileged workflow — only the canonical repo may run it. - if: inputs.worker == '' && (inputs.base_sha != '' || inputs.target_environment != 'production') && github.repository == 'Kilo-Org/cloud' + if: inputs.worker == '' && inputs.base_sha != '' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 outputs: