Hive Hive
Sign in

ci: deploy, release and scheduled workflows fire inside forks and spam contributors with failure notifications

GitHub issue · Open

Metadata
Source
tuist/tuist #12396
Updated
Aug 17, 2026
Details

Why is this needed?

When a contributor forks tuist/tuist and enables Actions — which you have to do to run CI on your own branches — every on: push: branches: [main] and on: schedule workflow starts firing in their fork. The deploy, release, infra-apply and notification workflows cannot possibly succeed there: they resolve secrets from Tuist’s 1Password vaults, kubectl apply against Tuist’s clusters, push to ghcr.io/tuist/*, or tag releases on tuist/tuist. So every main sync and every scheduled tick produces failed runs, and GitHub emails the fork owner about each one.

Observed in zunda-pixel/tuist:

Workflow Failures Trigger
Update Tuist CLI 12 schedule (daily)
Notify Slack on failure 2 workflow_run
Mgmt Cluster Apply 1 push main
Cache Deploy 1 push main

Example: https://github.com/zunda-pixel/tuist/actions/runs/32007898116/job/95321090383Load mgmt kubeconfig from 1Password fails because OP_SERVICE_ACCOUNT_TOKEN does not exist in the fork.

The scheduled ones are the worst offenders. Update Tuist CLI fails once a day, forever, whether or not the contributor is touching anything.

This is a different problem from the fork work already merged. #7640, #8701, #10198 and #11062 (and the closed #7986) are all about pull requests from a fork: those runs execute in tuist/tuist, so github.repository is tuist/tuist, and the only issue is that GitHub withholds secrets — handled today with github.event.pull_request.head.repo.fork != true on steps and mirrored *-fork jobs. This issue is about workflows running inside the fork on push / schedule / workflow_run. Note that github.event.pull_request.head.repo.fork is null on a push event, so … != true evaluates true inside a fork and is not a substitute for a repository check.

There is currently no repository guard anywhere in .github/workflows (a code search for github.repository == returns 0 hits), so all of these workflows are affected.

Steps to address the need

Add a fork guard to jobs that can only ever work on the upstream repository:

jobs:
apply:
name: kubectl apply
if: github.repository == 'tuist/tuist'

A guarded run is reported as skipped rather than failed, so GitHub sends no failure notification.

Workflows that need it (34):

app-release.yml, cache-deploy.yml, cache-release.yml, capi-provider-scaleway-applesilicon-image.yml, cli-release.yml, cnpg-restore-drill.yml, csi-deployment.yml, gradle-release.yml, grafana-datasource-release.yml, handbook.yml, hccm-deployment.yml, hetzner-robot-controller-image.yml, hetzner-robot-controller-release.yml, kura-controller-image.yml, l10n.yml, mgmt-cluster-apply.yml, noora-release.yml, noora-storybook-deployment.yml, notify-deploy-success.yml, notify-failure.yml, pomerium-deployment.yml, preview-platform-reconcile.yml, preview-sweep.yml, runners-controller-image.yml, runners-staging-smoke.yml, server-production-deployment.yml, skills-release.yml, slack-deployment.yml, stable-egress-controller-image.yml, stale.yml, status-deploy.yml, tuist-ops-deployment.yml, typesense-deployment.yml, update-tuist-cli.yml

Conventions that matter while doing it — these are the parts that are easy to get wrong:

  1. Guard the root jobs (the ones with no needs). Downstream jobs are skipped automatically once their needs are skipped, so one guard usually covers a whole workflow. server-production-deployment.yml has 23 jobs but only check-releases needs the guard, because every always() job in it additionally requires needs.check-releases.result == 'success' or one of its outputs.

  2. Also guard any job whose if contains a status function (always(), !cancelled(), failure()). Those bypass the implicit “needs succeeded” check, so they would still run against a skipped root job. The deploy jobs in noora-storybook-deployment.yml, pomerium-deployment.yml, slack-deployment.yml, tuist-ops-deployment.yml and typesense-deployment.yml are the cases. Keep the status function leading so its special meaning stays obvious:

    if: always() && github.repository == 'tuist/tuist' && (needs.build.result == 'success' || needs.build.result == 'skipped')
  3. Prefer a step-level guard when the job is also useful in a fork. handbook.yml should keep building the site everywhere — that is where dead-link validation happens — and guard only the two Cloudflare deploy steps, which are already conditioned on github.ref == 'refs/heads/main'.

  4. Leave CI workflows alone (cli.yml, server.yml, cache.yml, kura.yml, …). Those are supposed to run in forks. Most target self-hosted tuist-* runners that do not exist in a fork, so they queue and get cancelled rather than failing, and the ones that do run on ubuntu-latest are self-contained.

  5. Leave workflow_dispatch- and workflow_call-only workflows alone. They never fire on their own, so they are not a source of notification noise.

  6. Do not guard jobs that build without publishing. codebase-search.yml’s image job uses push: false, so it is a pure build check that works fine in a fork and is worth keeping there. Guarding it would remove validation for no benefit.

It is also worth documenting the two distinct fork cases somewhere under .github/, since the existing fork-related fixes all addressed only the first one and the two are easy to conflate.

I have this implemented and validated locally (actionlint 1.7.12 reports no new findings; the three remaining warnings in pomerium-deployment.yml, stale.yml and search.yml are pre-existing on main). Happy to open a PR if the approach looks right.

In the meantime, the workaround for fork owners is to disable the offending workflows individually from the fork’s Actions tab.

Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

Comments

No GitHub comments yet.