Hive
ci: deploy, release and scheduled workflows fire inside forks and spam contributors with failure notifications
GitHub issue · Open
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/95321090383 — Load 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:
-
Guard the root jobs (the ones with no
needs). Downstream jobs are skipped automatically once theirneedsare skipped, so one guard usually covers a whole workflow.server-production-deployment.ymlhas 23 jobs but onlycheck-releasesneeds the guard, because everyalways()job in it additionally requiresneeds.check-releases.result == 'success'or one of its outputs. -
Also guard any job whose
ifcontains a status function (always(),!cancelled(),failure()). Those bypass the implicit “needs succeeded” check, so they would still run against a skipped root job. Thedeployjobs innoora-storybook-deployment.yml,pomerium-deployment.yml,slack-deployment.yml,tuist-ops-deployment.ymlandtypesense-deployment.ymlare 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') -
Prefer a step-level guard when the job is also useful in a fork.
handbook.ymlshould keep building the site everywhere — that is where dead-link validation happens — and guard only the two Cloudflare deploy steps, which are already conditioned ongithub.ref == 'refs/heads/main'. -
Leave CI workflows alone (
cli.yml,server.yml,cache.yml,kura.yml, …). Those are supposed to run in forks. Most target self-hostedtuist-*runners that do not exist in a fork, so they queue and get cancelled rather than failing, and the ones that do run onubuntu-latestare self-contained. -
Leave
workflow_dispatch- andworkflow_call-only workflows alone. They never fire on their own, so they are not a source of notification noise. -
Do not guard jobs that build without publishing.
codebase-search.yml’simagejob usespush: 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.
Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.
Start a Flight and preserve its objective, outcome, and session here.
No GitHub comments yet.