Files
twenty/.github/workflows/ci-utils.yaml
Charles Bochet f416f81548 fix(ci): retry Danger.js on transient GitHub API fetch errors (#22151)
## Problem

The `danger-js` job in **CI Utils** has been failing across most PRs.
The failure is not a real Danger violation — it's a transient network
error fetching the PR diff/commits from the GitHub API:

```
Failed to fetch GitHub pull request files: FetchError: Invalid response body while trying to fetch
https://api.github.com/repos/twentyhq/twenty/pulls/XXXXX/files?page=1&per_page=100: Premature close
    at Gunzip.<anonymous> (.../node_modules/node-fetch/lib/index.js:400:12)
  errno: 'ERR_STREAM_PREMATURE_CLOSE',
  code: 'ERR_STREAM_PREMATURE_CLOSE'
```

GitHub closes the gzipped HTTP response mid-stream, and Danger's bundled
`node-fetch` has **no retry** on a dropped connection — so any single
blip fails the whole check.

## Why is this happening now?

Nothing on our side changed at the boundary where failures started. The
Node 24.16 bump landed Jun 8 (the job stayed green for 2+ weeks after),
Danger has been pinned at `13.0.4` for months, and there are **zero
commits** to `.nvmrc`, `twenty-utils/package.json`, or the
`yarn-install` action since Jun 22.

What changed is GitHub's API reset rate, and it changed abruptly:

| Day | Failures | Successes | Failure rate |
|-----|----------|-----------|--------------|
| Jun 23 | 1 | 48 | ~2% (green) |
| Jun 24 | 38 | 204 | ~16% |
| Jun 25 | 23 | 25 | **~48%** |

The same PR passes on one run and fails on the next (e.g. one PR shows
up as both pass and fail; another failed 3 runs in a row) — a code bug
can't flip outcomes on identical input, only an infrastructure flake
can. When GitHub's connection-reset rate was ~0% we never noticed; now
that it's in the tens of percent, roughly half of all PRs trip it.

## Fix

Wrap the Danger invocation in a small retry loop (3 attempts, 5s
backoff) so the check absorbs these transient fetch errors instead of
red-flagging the PR. Applied to both the `danger-js` and `congratulate`
jobs since they share the same failure mode.

This is the correct mitigation rather than a code revert — there's no
change on our side to revert. 3 attempts drop a ~48% single-shot failure
rate to ~11%, and a less-degraded ~16% rate to well under 1%. If
GitHub's reliability recovers, the retries simply stop firing and cost
nothing.

This is a CI-only change — no application code is touched.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22151?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 11:27:51 +02:00

76 lines
2.6 KiB
YAML

name: CI Utils
on:
# it's usually not recommended to use pull_request_target
# but we consider it's safe here if we keep the same steps
# see: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
# and: https://github.com/facebook/react-native/pull/34370/files
pull_request_target:
types: [opened, synchronize, reopened, closed]
permissions:
checks: write
pull-requests: write
statuses: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# We don't cancel in-progress because this workflow is triggered on
# pull_request_target, which means the ref can be the same accross two PRs.
# cancel-in-progress: true
jobs:
danger-js:
timeout-minutes: 5
runs-on: ubuntu-latest
if: github.event.action != 'closed'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Utils / Run Danger.js
# Danger's bundled node-fetch intermittently fails to decode gzipped
# GitHub API responses with ERR_STREAM_PREMATURE_CLOSE. Retry to absorb
# those transient fetch errors instead of failing the whole check.
run: |
cd packages/twenty-utils
for attempt in 1 2 3; do
if npx nx danger:ci; then
exit 0
fi
if [ "${attempt}" -lt 3 ]; then
echo "Danger.js attempt ${attempt} failed, retrying in 5s..."
sleep 5
fi
done
echo "Danger.js failed after 3 attempts"
exit 1
env:
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
congratulate:
timeout-minutes: 3
runs-on: ubuntu-latest
if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run congratulate-dangerfile.js
# Same transient ERR_STREAM_PREMATURE_CLOSE protection as danger-js.
run: |
cd packages/twenty-utils
for attempt in 1 2 3; do
if npx nx danger:congratulate; then
exit 0
fi
if [ "${attempt}" -lt 3 ]; then
echo "Danger.js congratulate attempt ${attempt} failed, retrying in 5s..."
sleep 5
fi
done
echo "Danger.js congratulate failed after 3 attempts"
exit 1
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}