diff --git a/.github/workflows/auto-docs.yml b/.github/workflows/auto-docs.yml index 2046bc5f6d..e3ef77dedf 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 65f719c8f9..a1f92bc289 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 598ed9c889..9dd7c31001 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 permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ebb0a840f0..9ac4c9b710 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 75bd385f77..9731cb8981 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 55d45973a2..638341a006 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -20,13 +20,18 @@ 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 check-production-db-startup: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # 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 environment: production @@ -59,7 +64,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 +98,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 +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 @@ -114,6 +122,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 }} @@ -124,6 +133,8 @@ 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' uses: ./.github/workflows/promote-vercel-deployment.yml with: deployment_url: ${{ needs.stage-ai-gateway.outputs.deployment_url }} @@ -149,6 +160,7 @@ jobs: ] if: >- ${{ + github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.check-changes.outputs.should_deploy == 'true' && needs.check-production-db-startup.result == 'success' && @@ -168,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: @@ -182,7 +194,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: @@ -255,6 +268,7 @@ jobs: ] if: >- ${{ + github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.check-changes.outputs.should_deploy == 'true' && needs.check-production-db-startup.result == 'success' && @@ -332,6 +346,7 @@ jobs: ] if: >- ${{ + github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.check-changes.outputs.should_deploy == 'true' && needs.check-production-db-startup.result == 'success' && diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 0c2de544c7..33c181f040 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -22,7 +22,10 @@ jobs: check-staging-db-startup: needs: check-changes - if: needs.check-changes.outputs.should_deploy == 'true' + # 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 environment: staging @@ -55,7 +58,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 +92,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 +104,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 @@ -130,6 +136,7 @@ jobs: ] if: >- ${{ + github.repository == 'Kilo-Org/cloud' && !cancelled() && needs.check-changes.outputs.should_deploy == 'true' && needs.check-staging-db-startup.result == 'success' && diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index f284154eb0..21b6786a5e 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -47,7 +47,11 @@ concurrency: jobs: deploy-manual: - if: inputs.worker != '' + # 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: ${{ inputs.worker == 'services/cloud-agent-next' && 75 || inputs.worker == 'services/auto-routing-benchmark' && inputs.target_environment == 'production' && 60 || 25 }} name: Deploy ${{ inputs.worker }} @@ -148,7 +152,8 @@ jobs: command: ${{ steps.runtime.outputs.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 != '' && github.repository == 'Kilo-Org/cloud' runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }} timeout-minutes: 5 outputs: @@ -314,7 +319,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: ${{ matrix.worker == 'services/cloud-agent-next' && 75 || matrix.worker == 'services/auto-routing-benchmark' && inputs.target_environment == 'production' && 60 || 25 }} environment: ${{ inputs.target_environment }} diff --git a/.github/workflows/extension-store-release.yml b/.github/workflows/extension-store-release.yml index a47cdf3259..251c00bce9 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 46c1083f37..9132daa53f 100644 --- a/.github/workflows/kilo-app-release.yml +++ b/.github/workflows/kilo-app-release.yml @@ -34,6 +34,11 @@ 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 permissions: @@ -153,12 +158,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 +228,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: @@ -470,7 +475,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: @@ -556,7 +561,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/promote-vercel-deployment.yml b/.github/workflows/promote-vercel-deployment.yml index 7fa1115ce2..ae8a7da542 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 4e6aec116b..7b5a47868d 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 99663cf2e7..7ce65c812c 100644 --- a/.github/workflows/redeploy-web.yml +++ b/.github/workflows/redeploy-web.yml @@ -12,6 +12,11 @@ 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 @@ -25,6 +30,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 +40,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 }} diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 84b92eab8d..f78b074520 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 b75f5bcb46..fd00443594 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 ebae24863d..ba002fef0b 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 9f4d65f028..d8716edd44 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