chore(maintenance): make fork divergence and static-analysis scope deliberate

Two problems made this fork expensive to maintain: scanner noise nobody
could act on, and divergence from upstream that nothing tracked.

Code scanning reported 3,518 open alerts. 3,449 were fallow findings and
2,399 of those sat in vendored bundles `.fallow.toml` never listed
(jolt-physics.wasm.js 436, box2djs/box2d.js 328, the Firebase SDK 655,
Recast, PeerJS, draco). Listing them plus raising the CRAP thresholds from
the defaults to evidence-based values takes fallow from 3,449 findings to
155, all in maintained code. The remaining 69 were CodeQL: 8 third-party
actions are now pinned to full commit SHAs with `# vN` comments (so
Dependabot keeps updating them) and all 10 workflows declare an explicit
least-privilege `permissions:` block.

SonarCloud's 2,845 open issues are mostly not this fork's code — 2,526 sit
in files byte-identical to upstream. The real defect was configuration:
~200 lines of `sonar.issue.ignore.multicriteria.e1..e88` in
.sonarcloud.properties, plus scripts/sonarcloud-apply-suppressions.sh, were
inert. That key is a Project Settings UI setting and cannot be read from
that file under automatic analysis, so two phases of remediation work
chased config the server never saw. Both are deleted; the exclusions that
do work are kept and extended. `sonar.test.inclusions` no longer declares
production source directories as test inclusions.

Divergence is now bounded and checked. `fork-divergence.json` is a
checked-in allowlist of the 111 modified, 51 fork-only and 7
upstream-missing paths, and `scripts/check-fork-divergence.js` fails CI on
divergence that is not listed or on stale entries. It reads upstream
through the GitHub trees API because 4ian/GDevelop is ~1.7GB and this runs
on every PR. Restoring Extensions/Physics3DBehavior/tests/
Physics3DRuntimeBehavior.spec.js — deleted by an earlier SonarCloud pass to
silence one rule — removes one entry each.

Repairs found along the way: every `// nosonar` marker sat on the line
before the statement SonarCloud flags, where the directive does nothing;
each is now on the exact reported line. Four garbled comment fragments left
in multiplayertools.ts by the same pass are gone. A two-line re-export shim
with no importers (AiGeneration/CustomAIClient.js) is deleted.

`scripts/sonarcloud-drift.js` now writes its outputs to $GITHUB_OUTPUT and
seeds its own baseline, so the weekly drift job's ceiling can actually fire;
it previously ran behind `continue-on-error` against a threshold that
predated the exclusions and was unreachable.

Root documentation collapses to MAINTENANCE.md. The three SonarCloud phase
plans and the Dependabot plan described steps that were proven impossible
and are deleted; AI_GAME_STUDIO_ASSESSMENT.md is a design record and stays.

Verified: newIDE/app 176/176 suites and 1,922 tests pass; GDevelop.js 33/33
suites and 1,177 tests pass; GDJS Karma 1,287 pass with the same 9
pre-existing failures (Firebase network tests) reproduced on a pristine
tree. check-format lists 37 files against the documented 41-file baseline,
so no new violations. Lockfile, drift and divergence guards all exit 0.
This commit is contained in:
John Smith
2026-09-22 20:01:28 -04:00
parent f72bb71f60
commit 48ed4e38d0
35 changed files with 1182 additions and 2037 deletions
+74 -3
View File
@@ -4,10 +4,13 @@
# generated test fixtures) so they don't pollute the static analysis
# results. These mirror the .sonarcloud.properties exclusions.
#
# ignorePatterns is honoured: a path listed here produces zero findings.
# Verified 2026-09-22 against the live code-scanning alert set — every
# path already listed reported exactly 0 alerts, while the vendored
# bundles added below accounted for 2,399 of the 3,449 fallow alerts.
#
# Per-rule/per-pattern suppression isn't a fallow config feature (it
# uses inline `// fallow-ignore-next-line <kind>` markers instead). The
# corresponding per-rule SonarCloud suppressions live in the
# .sonarcloud.properties file (or its in-UI multicriteria block).
# uses inline `// fallow-ignore-next-line <kind>` markers instead).
ignorePatterns = [
# Vendored / bundled JS (Emscripten, spine-pixi, monaco, three, pixi,
@@ -33,6 +36,8 @@ ignorePatterns = [
# Generated Emscripten bindings
"GDevelop.js/Bindings/Wrapper.cpp",
"GDevelop.js/Bindings/postjs.js",
"GDevelop.js/Bindings/glue.cpp",
"GDevelop.js/Bindings/glue.js",
# Vendored editor assets shipped with newIDE/app
"newIDE/app/public/external/**",
# Generated codemod output and test fixtures
@@ -52,8 +57,74 @@ ignorePatterns = [
"GDJS/tests/games/**",
# Generated localization files
"newIDE/app/src/locales/**",
# ---------------------------------------------------------------------
# Vendored third-party bundles not previously listed. These are the files
# .gitattributes already marks `linguist-vendored`, plus the Emscripten /
# Firebase / Recast / Jolt / pixi-filters build outputs shipped verbatim.
# Every one is byte-identical to its upstream copy; none is hand-edited.
# ---------------------------------------------------------------------
# Physics engine builds (box2d, Box2D wasm, Jolt wasm)
"Extensions/Physics3DBehavior/*.wasm.js",
"Extensions/Physics3DBehavior/jolt-physics.wasm.js",
"Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.js",
"Extensions/Physics2Behavior/box2d.js",
"Extensions/PhysicsBehavior/Box2D/**",
"Extensions/PhysicsBehavior/box2djs/**",
# Firebase SDK bundles (A_ = base, B_ = per-product build)
"Extensions/Firebase/A_firebasejs/**",
# Recast/Detour navmesh wasm build
"Extensions/NavMeshPathfinding/recast-navigation-generators.js",
"Extensions/NavMeshPathfinding/recast-navigation.wasm.js",
# PeerJS bundles
"Extensions/P2P/A_peer.js",
"Extensions/Multiplayer/peer.js",
# Other vendored minified bundles
"Extensions/BBText/pixi-multistyle-text/**",
"Extensions/DialogueTree/bondage.js/**",
"Extensions/ParticleSystem/SPARK/**",
"Extensions/ParticleSystem/pixi-particles-pixi-renderer.min.js",
"Extensions/PrimitiveDrawing/pixi-graphics-extras/**",
"Extensions/Shopify/shopify-buy.umd.polyfilled.min.js",
"Extensions/SpatialSound/howler.spatial.min.js",
"Extensions/TileMap/helper/TileMapHelper.js",
"Extensions/TileMap/pako/**",
"Extensions/TileMap/pixi-tilemap/**",
# Draco decoder wrapper shipped with the pixi renderer
"GDJS/Runtime/pixi-renderers/draco/**",
# Vendored pixi-filters builds (the whole directory is a vendored copy)
"Extensions/Effects/pixi-filters/**",
# Vendored SHA implementations (third-party, kept verbatim)
"Extensions/Leaderboards/sha256.js",
"newIDE/app/src/Utils/Sha256.js",
"newIDE/app/src/Utils/Crypto/sha512.js",
# Generated TypeScript declarations for the C++ core
"GDevelop.js/types/**",
"GDevelop.js/types.d.ts",
]
# Complexity / CRAP thresholds.
#
# The defaults (maxCrap 30, maxCyclomatic 20, maxCognitive 15) were written
# for a small greenfield project. Measured on this tree on 2026-09-22, the
# default maxCrap of 30 reported 926 functions in non-vendored code — a
# number nobody can act on, dominated by code this fork does not own (89% of
# open findings sit in files byte-identical to upstream).
#
# Raised to values that keep only genuine outliers:
# maxCrap 200 -> 149 findings remain (from 926)
# maxCyclomatic 25 -> cyclomatic 25+ only
# maxCognitive 25 -> drops the cognitive-15..24 band
# maxUnitSize 200 -> generous; test `describe()` blocks are exempted below
#
# Re-derive these from `fallow health` output rather than copying them if the
# tree changes materially.
[health]
maxCrap = 200
maxCyclomatic = 25
maxCognitive = 25
maxUnitSize = 200
# Inline markers used in source for per-line/per-file suppression:
# // fallow-ignore-next-line <kind>
# // fallow-ignore-file <kind>
+28 -8
View File
@@ -1,14 +1,34 @@
# CodeQL configuration for this fork.
#
# Paths to analyse live here (not in the workflow file) because
# `paths` is a setting of the CodeQL config file, not a direct input to
# `github/codeql-action/init`. See
# https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning
# for the supported inputs.
# !!! THIS FILE IS NOT READ BY THE CURRENT CODE SCANNING SETUP !!!
#
# Only the JS/TS sources that ship user-facing code are scanned. The C++
# tree (Core/, GDJS/Runtime/Cpp*, etc.) is covered by separate tools in
# .circleci/config.yml and excluded here to avoid duplicate findings.
# This repository uses CodeQL *default setup* (verified: the code scanning
# configuration lists languages actions, c-cpp, javascript, python and
# typescript with the `extended` query suite). Default setup does NOT read a
# repository's `.github/codeql-config.yml`; it only honours a config file
# named by the `github-codeql-config-file` *repository property*, which must
# be created at the organisation level and is currently unset (the org
# exposes no custom properties).
#
# Evidence that this file is inert: it lists `**/__tests__/**` under
# `paths-ignore`, yet code scanning reports an open alert inside
# `GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js` and inside
# `GDJS/tests/games/iframe-playground/*.html`.
#
# The `paths` list below is therefore documentation of intent, not enforcement.
# Two ways to make it real, pick one when it matters:
#
# 1. Create the org repository property so default setup picks this file up:
# gh api --method PATCH orgs/Heretek-AI/properties/schema \
# -f 'properties[][property_name]=github-codeql-config-file' ...
# then set it per repository to `.github/codeql-config.yml`. Requires org
# admin; see GitHub's "Repository properties for code scanning".
# 2. Switch code scanning to advanced setup (a workflow that calls
# `github/codeql-action/init` with `config-file:`). More moving parts and
# a workflow to maintain on every sync.
#
# Until then, exclude paths by fixing the alerts or dismissing them with a
# reason in the Security tab, not by editing this file.
name: "GDevelop Heretek JS/TS security-and-quality"
paths:
+1 -1
View File
@@ -131,7 +131,7 @@ jobs:
ls -lh release-files/
- name: Create Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}
name: GDevelop BYOK ${{ github.event.inputs.release_tag || github.ref_name }}
+7 -2
View File
@@ -22,6 +22,11 @@ on:
# to publish on Chromatic (not done by default).
workflow_dispatch:
# Minimal least-privilege permissions: builds Storybook, uploads it as an
# artifact and (optionally) syncs it to S3 / Chromatic with their own
# credentials. No repository write.
permissions:
contents: read
jobs:
build-storybook:
runs-on: ubuntu-latest
@@ -55,7 +60,7 @@ jobs:
- name: Configure AWS Credentials
if: steps.decide.outputs.should-publish == 'true'
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
with:
aws-access-key-id: ${{ secrets.BUILD_STORYBOOK_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.BUILD_STORYBOOK_AWS_SECRET_ACCESS_KEY }}
@@ -100,7 +105,7 @@ jobs:
- name: Publish Storybook to Chromatic
if: github.event_name == 'workflow_dispatch' && env.SHOULD_PUBLISH_CHROMATIC == 'true'
continue-on-error: true
uses: chromaui/action@v1
uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # v1
with:
workingDir: newIDE/app
storybookBuildDir: "build-storybook"
+27 -3
View File
@@ -2,10 +2,11 @@
#
# What this runs:
# 1. Lockfile security guard (scripts/security/check-lockfiles.js).
# 2. libGD.js (Emscripten) build, once, then shared via artifact.
# 3. Jest test suites in newIDE/app and GDevelop.js, both consuming the
# 2. Fork-divergence guard (scripts/check-fork-divergence.js).
# 3. libGD.js (Emscripten) build, once, then shared via artifact.
# 4. Jest test suites in newIDE/app and GDevelop.js, both consuming the
# build output.
# 4. Lint + Flow type checks for newIDE/app.
# 5. Lint + Flow type checks for newIDE/app.
#
# Karma/Mocha tests for GDJS and SharedLibs/TileMapHelper require a
# headless browser and are run in the dedicated CI jobs in
@@ -44,6 +45,29 @@ jobs:
- name: Run guard
run: node scripts/security/check-lockfiles.js
fork-divergence:
name: Fork divergence guard
# Keeps the fork's divergence from 4ian/GDevelop bounded and deliberate.
# `fork-divergence.json` is the checked-in allowlist; this job fails when a
# path diverges that is not listed (new, undecided divergence) or when a
# listed path no longer diverges (stale entry). Without it, divergence only
# surfaces as a merge conflict on the next upstream sync.
#
# The upstream tree is read through the GitHub trees API rather than a
# clone: 4ian/GDevelop is ~1.7 GB and this job runs on every PR.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Compare against upstream 4ian/GDevelop
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/check-fork-divergence.js
lockfile-sync:
name: Lockfile sync (all manifests)
# `npm ci` hard-fails when package.json and package-lock.json disagree, but
@@ -11,6 +11,10 @@ on:
# Allows to run this workflow manually from the Actions tab.
workflow_dispatch:
# Minimal least-privilege permissions: extracts strings locally and uploads
# them to Crowdin with a Crowdin token. No repository write.
permissions:
contents: read
jobs:
extract-translations:
if: github.repository == '4ian/GDevelop'
+33 -15
View File
@@ -34,7 +34,7 @@ jobs:
with:
node-version: "20"
- name: Run fallow audit
uses: fallow-rs/fallow@v3
uses: fallow-rs/fallow@bd8fca5af5df4ccfd94c7a835d17bd93c31a7cef # v3
with:
version: "3.27.0"
command: audit
@@ -140,23 +140,41 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "20"
# `sonarcloud-drift.js` owns both the fetch and the threshold, and now
# writes `count` / `baseline` / `seeded` to $GITHUB_OUTPUT. It also seeds
# the baseline itself when the file is absent, which is why the separate
# "Seed baseline if missing" step that used to sit here is gone: it was
# gated on `steps.drift.outputs.outcome == 'skipped'`, which is not a
# real value (a step exposes its result as `steps.<id>.outcome`; the
# script never wrote any output at all). It could therefore never run.
- name: Run drift check
id: drift
run: node scripts/sonarcloud-drift.js
- name: Seed baseline if missing
if: steps.drift.outputs.outcome == 'skipped'
run: node scripts/sonarcloud-drift.js --update-baseline
- name: Threshold guard
continue-on-error: true # informational for now: SonarCloud open count is currently above threshold; refresh suppressions (see SONARCLOUD_REMEDIATION_PLAN.md) before re-gating
# Phase 4 acceptance: open count should stay under 4,500. If it
# climbs above that, the suppressions need refreshing or new
# patterns are leaking in. Update SONARCLOUD_REMEDIATION_PLAN.md
# if the threshold moves.
# Ceiling on the absolute count, independent of the growth percentage.
#
# This is deliberately NOT `continue-on-error`. The pre-cleanup value of
# 1,500 was chosen before the vendored/generated paths were excluded and
# was never achievable; the fork measured 2,845 open at the baseline
# recorded in `.sonarcloud-drift-baseline`. The ceiling below is that
# baseline plus ~10% headroom, so it fires on real drift (a new rule
# class, a re-enabled path, a dependency that pulls in unignored code)
# rather than on the standing tail of upstream-identical code smells.
#
# Raise it deliberately, together with `--update-baseline`, when a change
# legitimately adds issues; never to make a red run green.
- name: Absolute-count ceiling
run: |
TOTAL=$(curl -fs "https://sonarcloud.io/api/issues/search?componentKeys=Heretek-AI_GDevelop-Heretek&issueStatuses=OPEN&ps=1" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['total'])")
echo "Open SonarCloud issues: $TOTAL"
if [ "$TOTAL" -gt 1500 ]; then
echo "::error::SonarCloud open count $TOTAL exceeds 1,500 threshold"
COUNT="${{ steps.drift.outputs.count }}"
BASE="${{ steps.drift.outputs.baseline }}"
CEILING=$(( BASE + BASE / 10 ))
echo "Open SonarCloud issues: $COUNT (baseline $BASE, ceiling $CEILING)"
if [ "$COUNT" -gt "$CEILING" ]; then
echo "::error::SonarCloud open count $COUNT exceeds the ceiling of $CEILING (baseline $BASE + 10%). See MAINTENANCE.md."
exit 1
fi
- name: Remind when the baseline was just seeded
if: steps.drift.outputs.seeded == 'true'
run: |
echo "::warning::.sonarcloud-drift-baseline was missing and has been seeded with ${{ steps.drift.outputs.count }}. Commit it."
+5 -1
View File
@@ -9,13 +9,17 @@ on:
release:
types: [published]
# Minimal least-privilege permissions: dispatches to another repository
# using a PAT, so the default GITHUB_TOKEN needs no write scope.
permissions:
contents: read
jobs:
dispatch-event:
if: github.repository == '4ian/GDevelop'
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
with:
token: ${{ secrets.GDCORE_TOOLS_PAT }}
repository: arthuro555/gdcore-tools
+6 -1
View File
@@ -2,13 +2,18 @@ name: GDevelop Issues automatic workflow
on:
issues:
types: [opened]
# Minimal least-privilege permissions: this workflow only reads the new
# issue's body from the event payload and posts one comment.
permissions:
contents: read
issues: write
jobs:
autocomment:
runs-on: ubuntu-latest
if: contains(github.event.issue.body, 'The node to be removed is not a child of this node')
steps:
- name: Autocomment indications on bug if it looks like #3453
uses: peter-evans/create-or-update-comment@v3
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
@@ -11,6 +11,11 @@ on:
- "**" # Don't run on new tags
workflow_dispatch: # Allows manual triggering from the Actions tab
# Minimal least-privilege permissions: opens a pull request with the
# extension translations copied from GDevelop-extensions.
permissions:
contents: write
pull-requests: write
jobs:
update-extension-translations:
if: github.repository == '4ian/GDevelop'
@@ -37,7 +42,7 @@ jobs:
cp /tmp/GDevelop-extensions/.translations/LocalesMetadata.js newIDE/app/src/locales/ExtensionLocalesMetadata.js
- name: Create Pull Request with updated translations
uses: peter-evans/create-pull-request@v6
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6
with:
commit-message: Update extension translations [skip ci]
branch: chore/update-extension-translations
+6 -1
View File
@@ -12,6 +12,11 @@ on:
# Allows to run this workflow manually from the Actions tab.
workflow_dispatch:
# Minimal least-privilege permissions: opens a pull request with the
# translations downloaded from Crowdin.
permissions:
contents: write
pull-requests: write
jobs:
update-translations:
if: github.repository == '4ian/GDevelop'
@@ -69,7 +74,7 @@ jobs:
run: test -f newIDE/app/src/locales/en/messages.js
- name: Create a Pull Request with the changes
uses: peter-evans/create-pull-request@v6
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6
with:
commit-message: Update translations [skip ci]
branch: chore/update-translations
+28
View File
@@ -81,6 +81,34 @@ jobs:
echo "sync_branch=$SYNC_BRANCH" >> $GITHUB_OUTPUT
echo "merge_success=$MERGE_SUCCESS" >> $GITHUB_OUTPUT
- name: Verify fork divergence is unchanged by the sync
# A sync merge can silently re-introduce upstream copies of files the
# fork deliberately deletes or modifies (it is how
# Extensions/Physics3DBehavior/tests/Physics3DRuntimeBehavior.spec.js
# was deleted by hand and how the obsolete wavesurfer patch keeps
# coming back). `fork-divergence.json` is the checked-in allowlist of
# intended divergence; this step reports any drift instead of letting
# it reach a reviewer as a mystery diff. Informational only: the
# reviewer decides whether to record the drift or revert it.
if: steps.check_diff.outputs.commits_count != '0'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if node scripts/check-fork-divergence.js; then
echo "Divergence matches fork-divergence.json."
else
{
echo "### Fork divergence changed in this sync"
echo ""
echo "The sync introduced or removed divergence not recorded in"
echo "\`fork-divergence.json\`. Review the list above, then either"
echo "record it (\`node scripts/check-fork-divergence.js --update\`)"
echo "or revert the accidental part before merging."
} >> "$GITHUB_STEP_SUMMARY"
echo "::warning::Fork divergence changed; see the job summary."
fi
- name: Open or Update Sync PR
if: steps.check_diff.outputs.commits_count != '0'
run: |
+5
View File
@@ -0,0 +1,5 @@
{
"count": 2845,
"recordedAt": "2026-09-22T23:44:25.919Z",
"source": "scripts/sonarcloud-drift.js"
}
+63 -364
View File
@@ -1,7 +1,28 @@
# SonarCloud configuration for the Heretek-AI/GDevelop-Heretek fork.
#
# Excludes paths we don't maintain (vendored bundles, codemod output,
# generated test fixtures) so they don't pollute the issue count.
# !!! READ THIS BEFORE ADDING RULE-LEVEL SUPPRESSIONS !!!
#
# This project uses SonarCloud *automatic analysis* (verified: the project
# setting `sonar.autoscan.enabled` is `true`). Under automatic analysis:
#
# * Only the keys documented for `.sonarcloud.properties` are honoured —
# `sonar.sources`, `sonar.exclusions`, `sonar.inclusions`, `sonar.tests`,
# `sonar.test.exclusions`, `sonar.test.inclusions`, `sonar.sourceEncoding`,
# `sonar.cpd.exclusions`, `sonar.python.version`.
# * `sonar.issue.ignore.multicriteria` (and its `sonar.issue.enforce.*`
# sibling) are NOT among them and CANNOT be set from this file. They are
# a Project Settings -> Issues -> Exclusions (UI) setting only.
#
# A previous revision of this file carried ~200 lines of commented-out
# `sonar.issue.ignore.multicriteria.e1..e88` entries. They were inert and
# were deleted; the same applies to `scripts/sonarcloud-apply-suppressions.sh`,
# which only printed them. Do not re-add either. If rule-level suppression is
# wanted later, that is a UI action or a switch to CI-based analysis.
#
# Path exclusions below ARE honoured — verified against the live issue set:
# a path listed here reports zero open issues while a sibling path outside
# the list reports them (e.g. filter-glitch.js = 0, filter-crt.js = 5).
#
# These mirror the .fallow.toml exclusions.
sonar.exclusions=\
@@ -10,6 +31,7 @@ sonar.exclusions=\
GDJS/Runtime/pixi-renderers/pixi.js,\
GDJS/Runtime/pixi-renderers/three.js,\
GDJS/Runtime/pixi-renderers/ThreeAddons.js,\
GDJS/Runtime/pixi-renderers/draco/**,\
GDJS/Runtime/howler-sound-manager/howler.min.js,\
GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver.js,\
newIDE/app/public/external/**,\
@@ -27,23 +49,25 @@ sonar.exclusions=\
newIDE/app/src/locales/**,\
SharedLibs/ThreeAddons/**,\
Extensions/Physics3DBehavior/JsExtension.js,\
Extensions/Effects/pixi-filters/filter-color-gradient.js,\
Extensions/Effects/pixi-filters/filter-glitch.js,\
Extensions/Effects/pixi-filters/**,\
Extensions/Firebase/B_firebasetools/**,\
Extensions/TileMap/JsExtension.js,\
Extensions/NavMeshPathfinding/JsExtension.js,\
GDevelop.js/Bindings/Wrapper.cpp,\
GDevelop.js/Bindings/postjs.js
GDevelop.js/Bindings/postjs.js,\
GDevelop.js/Bindings/glue.cpp,\
GDevelop.js/Bindings/glue.js,\
GDevelop.js/types/**,\
GDevelop.js/types.d.ts
# Test coverage paths (only include JS source we actively maintain)
sonar.test.inclusions=\
newIDE/app/src/**,\
Extensions/**/*.{js,ts},\
GDJS/Runtime/**/*.{js,ts},\
GDevelop.js/**/*.{js,ts},\
SharedLibs/**/*.{js,ts}
# Source code paths (exclude vendored + generated)
# Scope of the files analyzed as production source.
#
# `newIDE/app/src` (the editor UI, ~2,350 source files) is deliberately NOT
# listed as a test root and is not excluded either. It is currently absent
# from the SonarCloud index entirely — see MAINTENANCE.md for the measured
# state and the probe command. Do not "fix" that by adding it to
# `sonar.exclusions`: that would hide the fork's own AI/Studio code from
# analysis permanently, which is worse than the current gap.
sonar.sources=\
newIDE/app/src,\
newIDE/app/scripts,\
@@ -52,6 +76,31 @@ sonar.sources=\
GDevelop.js,\
SharedLibs
# Test scope.
#
# This block previously read `sonar.test.inclusions=newIDE/app/src/**,
# Extensions/**/*.{js,ts}, ...` under the comment "Test coverage paths (only
# include JS source we actively maintain)" — i.e. it declared *production*
# source directories as test inclusions. That is a misreading of the key:
# `sonar.test.inclusions` is a glob filter applied *within* `sonar.tests`, and
# `Extensions/**/*.{js,ts}` is not a valid Sonar glob (no brace expansion).
#
# Corrected form: `sonar.tests` names the roots that contain test files, and
# `sonar.test.inclusions` keeps only the actual test files in scope.
sonar.tests=\
Extensions,\
GDevelop.js,\
GDJS/tests,\
SharedLibs,\
newIDE/app/scripts
sonar.test.inclusions=\
**/*.spec.js,\
**/*.spec.ts,\
**/*.spec.tsx,\
**/__tests__/**,\
**/tests/**
# C++/Python/etc. are handled by separate tooling
sonar.c.file.suffixes=-
sonar.cpp.file.suffixes=-
@@ -65,353 +114,3 @@ sonar.coverage.exclusions=\
**/*.spec.ts,\
**/stories/**,\
newIDE/app/src/AiGeneration/AiRequestChat/ChatMessages.js
# =========================================================================
# Phase 3 — Per-rule suppressions (SonarCloud "Issue Exclusions")
# =========================================================================
#
# These are SERVER-SIDE settings on SonarCloud. The block below is
# documentation of what we want; the operator (you) pastes the
# equivalent into Project Settings → Issues → Exclusions → "Multi-
# criteria" → Add. The format is one entry per criterion, with a 4-letter
# code and the ruleKey + (resourceKey | componentKeys | paths) as
# glob/range.
#
# Each criterion maps to the same ignoreFindings entry in
# .fallow.toml. Review and adjust the globs as the codebase evolves.
# sonar.issue.ignore.multicriteria=e1,e2,e3,...
#
# --- e1: javascript:S3504 (var -> const/let) ---
# sonar.issue.ignore.multicriteria.e1.ruleKey=javascript:S3504
# sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.{js,ts}
#
# --- e2: javascript:S3776 (cognitive complexity) in AI + EditorFunctions ---
# sonar.issue.ignore.multicriteria.e2.ruleKey=javascript:S3776
# sonar.issue.ignore.multicriteria.e2.resourceKey=newIDE/app/src/EditorFunctions/**
# sonar.issue.ignore.multicriteria.e3.ruleKey=javascript:S3776
# sonar.issue.ignore.multicriteria.e3.resourceKey=newIDE/app/src/AiGeneration/**
#
# --- e4: typescript:S3776 (cognitive complexity) in NavMeshPathfinding + tests ---
# sonar.issue.ignore.multicriteria.e4.ruleKey=typescript:S3776
# sonar.issue.ignore.multicriteria.e4.resourceKey=Extensions/NavMeshPathfinding/**
# sonar.issue.ignore.multicriteria.e5.ruleKey=typescript:S3776
# sonar.issue.ignore.multicriteria.e5.resourceKey=GDJS/Runtime/gameplay-tests/**
#
# --- e6: javascript:S3358 (nested ternaries) ---
# sonar.issue.ignore.multicriteria.e6.ruleKey=javascript:S3358
# sonar.issue.ignore.multicriteria.e6.resourceKey=**/*.{js,ts}
#
# --- e7: javascript:S7773 (mixed tabs/spaces in fix-flow-errors.sh) ---
# sonar.issue.ignore.multicriteria.e7.ruleKey=javascript:S7773
# sonar.issue.ignore.multicriteria.e7.resourceKey=newIDE/app/scripts/fix-flow-errors.sh
#
# --- e8: javascript:S4138 / S6836 (loose assertions in tests) ---
# sonar.issue.ignore.multicriteria.e8.ruleKey=javascript:S4138
# sonar.issue.ignore.multicriteria.e8.resourceKey=**/*.spec.{js,ts}
# sonar.issue.ignore.multicriteria.e9.ruleKey=javascript:S6836
# sonar.issue.ignore.multicriteria.e9.resourceKey=**/*.spec.{js,ts}
#
# --- e10: javascript:S6441 / S1121 / S6747 / S7778 / S7769 / S3863 ---
# (one entry per rule, blanket suppress for the codebase style we accept)
# sonar.issue.ignore.multicriteria.e10.ruleKey=javascript:S6441
# ...
#
# --- e11: javascript:S1523 (eval/Func constr in LLM-script execution) ---
# sonar.issue.ignore.multicriteria.e11.ruleKey=javascript:S1523
# sonar.issue.ignore.multicriteria.e11.resourceKey=newIDE/app/src/EditorFunctions/ScriptExecution/**
# sonar.issue.ignore.multicriteria.e12.ruleKey=javascript:S1523
# sonar.issue.ignore.multicriteria.e12.resourceKey=newIDE/app/src/Utils/EventsGlobalSearchScanner.js
# sonar.issue.ignore.multicriteria.e13.ruleKey=typescript:S1523
# sonar.issue.ignore.multicriteria.e13.resourceKey=GDJS/Runtime/gameplay-tests/**
#
# --- e14: javascript:S2245 (Math.random for non-crypto IDs) ---
# sonar.issue.ignore.multicriteria.e14.ruleKey=javascript:S2245
# sonar.issue.ignore.multicriteria.e14.resourceKey=**/*.{js,ts}
#
# --- e15: shell:S6505 / githubactions:S6505 (--ignore-scripts) ---
# sonar.issue.ignore.multicriteria.e15.ruleKey=shell:S6505
# sonar.issue.ignore.multicriteria.e15.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e16.ruleKey=githubactions:S6505
# sonar.issue.ignore.multicriteria.e16.resourceKey=.github/workflows/build-release.yml
#
# --- e17: jssecurity:* (after the real fixes in Phase 2) ---
# sonar.issue.ignore.multicriteria.e17.ruleKey=jssecurity:S8707
# sonar.issue.ignore.multicriteria.e17.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e18.ruleKey=jssecurity:S7044
# sonar.issue.ignore.multicriteria.e18.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e19.ruleKey=jssecurity:S8476
# sonar.issue.ignore.multicriteria.e19.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e20.ruleKey=jssecurity:S2076
# sonar.issue.ignore.multicriteria.e20.resourceKey=newIDE/electron-app/app/**
# sonar.issue.ignore.multicriteria.e21.ruleKey=pythonsecurity:S2083
# sonar.issue.ignore.multicriteria.e21.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e22.ruleKey=jssecurity:S8701
# sonar.issue.ignore.multicriteria.e22.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e23.ruleKey=jssecurity:S8705
# sonar.issue.ignore.multicriteria.e23.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e24.ruleKey=jssecurity:S5144
# sonar.issue.ignore.multicriteria.e24.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e25.ruleKey=jssecurity:S6350
# sonar.issue.ignore.multicriteria.e25.resourceKey=newIDE/app/scripts/**
# sonar.issue.ignore.multicriteria.e26.ruleKey=javascript:S4036
# sonar.issue.ignore.multicriteria.e26.resourceKey=newIDE/app/scripts/**
# After pasting the e1-e26 block into SonarCloud → Project Settings →
# Issues → Exclusions, click "Reload" on the Quality Profiles tab and
# the open-issue count should drop from 10,289 to roughly 3,000-4,000.
# ---------------------------------------------------------------------------
# PHASE 2 suppressions (added after Phase 1 was merged at 3,649 open)
# ---------------------------------------------------------------------------
# Goal: drop to ~500-1,000 open. These target the rules that emerged
# after Phase 1's suppressions were applied. Each row maps to one
# multicriteria entry in the SonarCloud UI (paste the uncommented
# version of each line into the operator's setup).
#
# --- e27: typescript:S101 (class naming convention, 275) ---
# sonar.issue.ignore.multicriteria.e27.ruleKey=typescript:S101
# sonar.issue.ignore.multicriteria.e27.resourceKey=**/*.{js,ts}
#
# --- e28: typescript:S6582 (single-char names, 228) ---
# sonar.issue.ignore.multicriteria.e28.ruleKey=typescript:S6582
# sonar.issue.ignore.multicriteria.e28.resourceKey=**/*.{js,ts}
#
# --- e29: typescript:S1186 (empty methods, 209) ---
# sonar.issue.ignore.multicriteria.e29.ruleKey=typescript:S1186
# sonar.issue.ignore.multicriteria.e29.resourceKey=**/*.{js,ts}
#
# --- e30: javascript:S878 (== vs ===, 170) ---
# sonar.issue.ignore.multicriteria.e30.ruleKey=javascript:S878
# sonar.issue.ignore.multicriteria.e30.resourceKey=**/*.{js,ts}
#
# --- e31: typescript:S1135 (TODO markers, 144) ---
# sonar.issue.ignore.multicriteria.e31.ruleKey=typescript:S1135
# sonar.issue.ignore.multicriteria.e31.resourceKey=**/*.{js,ts}
#
# --- e32: javascript:S7773 (mixed tabs/spaces, 133) ---
# sonar.issue.ignore.multicriteria.e32.ruleKey=javascript:S7773
# sonar.issue.ignore.multicriteria.e32.resourceKey=**/*.{js,ts}
#
# --- e33: javascript:S3504 (var -> const/let, 131) ---
# sonar.issue.ignore.multicriteria.e33.ruleKey=javascript:S3504
# sonar.issue.ignore.multicriteria.e33.resourceKey=**/*.{js,ts}
#
# --- e34: typescript:S3776 (cognitive complexity, 123) ---
# sonar.issue.ignore.multicriteria.e34.ruleKey=typescript:S3776
# sonar.issue.ignore.multicriteria.e34.resourceKey=**/*.{ts,tsx}
# sonar.issue.ignore.multicriteria.e35.ruleKey=typescript:S3776
# sonar.issue.ignore.multicriteria.e35.resourceKey=Extensions/NavMeshPathfinding/**
# sonar.issue.ignore.multicriteria.e36.ruleKey=typescript:S3776
# sonar.issue.ignore.multicriteria.e36.resourceKey=Extensions/Physics2Behavior/**
# sonar.issue.ignore.multicriteria.e37.ruleKey=typescript:S3776
# sonar.issue.ignore.multicriteria.e37.resourceKey=Extensions/Physics3DBehavior/**
#
# --- e38: typescript:S7741 (refactor, 99) ---
# sonar.issue.ignore.multicriteria.e38.ruleKey=typescript:S7741
# sonar.issue.ignore.multicriteria.e38.resourceKey=**/*.{ts,tsx}
#
# --- e39: cpp:S3471 (C++ refactor, 90) ---
# sonar.issue.ignore.multicriteria.e39.ruleKey=cpp:S3471
# sonar.issue.ignore.multicriteria.e39.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e40: typescript:S2933 (TODO refactor, 82) ---
# sonar.issue.ignore.multicriteria.e40.ruleKey=typescript:S2933
# sonar.issue.ignore.multicriteria.e40.resourceKey=**/*.{ts,tsx}
#
# --- e41: javascript:S6679 (refactor, 78) ---
# sonar.issue.ignore.multicriteria.e41.ruleKey=javascript:S6679
# sonar.issue.ignore.multicriteria.e41.resourceKey=**/*.{js,ts}
#
# --- e42: typescript:S1874 (parameter naming, 72) ---
# sonar.issue.ignore.multicriteria.e42.ruleKey=typescript:S1874
# sonar.issue.ignore.multicriteria.e42.resourceKey=**/*.{ts,tsx}
#
# --- e43: typescript:S4138 (== vs ===, 72) ---
# sonar.issue.ignore.multicriteria.e43.ruleKey=typescript:S4138
# sonar.issue.ignore.multicriteria.e43.resourceKey=**/*.{ts,tsx}
#
# --- e44: cpp:S5416 (C++ refactor, 70) ---
# sonar.issue.ignore.multicriteria.e44.ruleKey=cpp:S5416
# sonar.issue.ignore.multicriteria.e44.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e45: cpp:S3230 (C++ refactor, 58) ---
# sonar.issue.ignore.multicriteria.e45.ruleKey=cpp:S3230
# sonar.issue.ignore.multicriteria.e45.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e46: typescript:S3358 (nested ternaries, 55) ---
# sonar.issue.ignore.multicriteria.e46.ruleKey=typescript:S3358
# sonar.issue.ignore.multicriteria.e46.resourceKey=**/*.{ts,tsx}
#
# --- e47: typescript:S6660 (refactor, 54) ---
# sonar.issue.ignore.multicriteria.e47.ruleKey=typescript:S6660
# sonar.issue.ignore.multicriteria.e47.resourceKey=**/*.{ts,tsx}
#
# --- e48: typescript:S6535 (refactor, 51) ---
# sonar.issue.ignore.multicriteria.e48.ruleKey=typescript:S6535
# sonar.issue.ignore.multicriteria.e48.resourceKey=**/*.{ts,tsx}
#
# --- e49: typescript:S6861 (add default case, 20) ---
# sonar.issue.ignore.multicriteria.e49.ruleKey=typescript:S6861
# sonar.issue.ignore.multicriteria.e49.resourceKey=**/*.{ts,tsx}
#
# --- e50: typescript:S7773 (mixed tabs/spaces in TS, 46) ---
# sonar.issue.ignore.multicriteria.e50.ruleKey=typescript:S7773
# sonar.issue.ignore.multicriteria.e50.resourceKey=**/*.{ts,tsx}
#
# --- e51: typescript:S1444 (private field, 46) ---
# sonar.issue.ignore.multicriteria.e51.ruleKey=typescript:S1444
# sonar.issue.ignore.multicriteria.e51.resourceKey=**/*.{ts,tsx}
#
# --- e52: cpp:S3490 (C++ refactor, 43 CRITICAL) ---
# sonar.issue.ignore.multicriteria.e52.ruleKey=cpp:S3490
# sonar.issue.ignore.multicriteria.e52.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e53: cpp:S5025 (C++ refactor, 10) ---
# sonar.issue.ignore.multicriteria.e53.ruleKey=cpp:S5025
# sonar.issue.ignore.multicriteria.e53.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e54: cpp:S3776 (C++ cognitive complexity, 5) ---
# sonar.issue.ignore.multicriteria.e54.ruleKey=cpp:S3776
# sonar.issue.ignore.multicriteria.e54.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e55: cpp:S1003 (C++ refactor, 5) ---
# sonar.issue.ignore.multicriteria.e55.ruleKey=cpp:S1003
# sonar.issue.ignore.multicriteria.e55.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e56: javascript:S5443 (CRITICAL, 1) ---
# Suppress after manual review; the single instance is in a generated test
# that mock-checks a method call, not a real injection vector.
# sonar.issue.ignore.multicriteria.e56.ruleKey=javascript:S5443
# sonar.issue.ignore.multicriteria.e56.resourceKey=**/*.spec.{js,ts}
#
# --- e57: jssecurity:S8701 (LLM sandbox, 1) ---
# sonar.issue.ignore.multicriteria.e57.ruleKey=jssecurity:S8701
# sonar.issue.ignore.multicriteria.e57.resourceKey=newIDE/app/scripts/**
#
# --- e58: jssecurity:S8705 (LLM sandbox, 1) ---
# sonar.issue.ignore.multicriteria.e58.ruleKey=jssecurity:S8705
# sonar.issue.ignore.multicriteria.e58.resourceKey=newIDE/app/scripts/**
#
# --- e59: jssecurity:S5144 (URL from user data, 1) ---
# sonar.issue.ignore.multicriteria.e59.ruleKey=jssecurity:S5144
# sonar.issue.ignore.multicriteria.e59.resourceKey=newIDE/app/scripts/**
#
# --- e60: jssecurity:S6350 (command injection, 2) ---
# sonar.issue.ignore.multicriteria.e60.ruleKey=jssecurity:S6350
# sonar.issue.ignore.multicriteria.e60.resourceKey=newIDE/app/scripts/**
#
# --- e61: javascript:S4036 (PATH injection, 5) ---
# sonar.issue.ignore.multicriteria.e61.ruleKey=javascript:S4036
# sonar.issue.ignore.multicriteria.e61.resourceKey=newIDE/app/scripts/**
#
# --- e62: text:S8564 (lock file missing, 2) ---
# sonar.issue.ignore.multicriteria.e62.ruleKey=text:S8564
# sonar.issue.ignore.multicriteria.e62.resourceKey=SharedLibs/TileMapHelper/package.json
# sonar.issue.ignore.multicriteria.e63.ruleKey=text:S8564
# sonar.issue.ignore.multicriteria.e63.resourceKey=SharedLibs/ThreeAddons/package.json
#
# --- e64: typescript:S107 (function too many parameters, 49) ---
# sonar.issue.ignore.multicriteria.e64.ruleKey=typescript:S107
# sonar.issue.ignore.multicriteria.e64.resourceKey=**/*.{ts,tsx}
#
# --- e65: javascript:S107 (JS function too many parameters, 8) ---
# sonar.issue.ignore.multicriteria.e65.ruleKey=javascript:S107
# sonar.issue.ignore.multicriteria.e65.resourceKey=**/*.{js,jsx}
#
# --- e66: typescript:S1121 (assignment in sub-expression, 34) ---
# sonar.issue.ignore.multicriteria.e66.ruleKey=typescript:S1121
# sonar.issue.ignore.multicriteria.e66.resourceKey=**/*.{ts,tsx}
#
# --- e67: typescript:S7762 (.remove() vs .removeChild(), 24) ---
# sonar.issue.ignore.multicriteria.e67.ruleKey=typescript:S7762
# sonar.issue.ignore.multicriteria.e67.resourceKey=**/*.{ts,tsx}
#
# --- e68: typescript:S6035 (replace alternation with character class, 23) ---
# sonar.issue.ignore.multicriteria.e68.ruleKey=typescript:S6035
# sonar.issue.ignore.multicriteria.e68.resourceKey=**/*.{ts,tsx}
#
# --- e69: typescript:S6671 (promise rejection should be Error, 20) ---
# sonar.issue.ignore.multicriteria.e69.ruleKey=typescript:S6671
# sonar.issue.ignore.multicriteria.e69.resourceKey=**/*.{ts,tsx}
#
# --- e70: javascript:S6557 (String#startsWith, 20) ---
# sonar.issue.ignore.multicriteria.e70.ruleKey=javascript:S6557
# sonar.issue.ignore.multicriteria.e70.resourceKey=**/*.{js,ts}
#
# --- e71: typescript:S6666 (spread vs apply, 17) ---
# sonar.issue.ignore.multicriteria.e71.ruleKey=typescript:S6666
# sonar.issue.ignore.multicriteria.e71.resourceKey=**/*.{ts,tsx}
#
# --- e72: typescript:S7765 (.includes() vs .some(), 17) ---
# sonar.issue.ignore.multicriteria.e72.ruleKey=typescript:S7765
# sonar.issue.ignore.multicriteria.e72.resourceKey=**/*.{ts,tsx}
#
# --- e73: typescript:S7778 (avoid chained push, 16) ---
# sonar.issue.ignore.multicriteria.e73.ruleKey=typescript:S7778
# sonar.issue.ignore.multicriteria.e73.resourceKey=**/*.{ts,tsx}
#
# --- e74: javascript:S7780 (String.raw, 15) ---
# sonar.issue.ignore.multicriteria.e74.ruleKey=javascript:S7780
# sonar.issue.ignore.multicriteria.e74.resourceKey=**/*.{js,ts}
#
# --- e75: typescript:S7750 (.find() vs filter()[0], 15) ---
# sonar.issue.ignore.multicriteria.e75.ruleKey=typescript:S7750
# sonar.issue.ignore.multicriteria.e75.resourceKey=**/*.{ts,tsx}
#
# --- e76: typescript:S6551 (default stringification, 13) ---
# sonar.issue.ignore.multicriteria.e76.ruleKey=typescript:S6551
# sonar.issue.ignore.multicriteria.e76.resourceKey=**/*.{ts,tsx}
#
# --- e77: typescript:S7740 (avoid this = harness, 11) ---
# sonar.issue.ignore.multicriteria.e77.ruleKey=typescript:S7740
# sonar.issue.ignore.multicriteria.e77.resourceKey=**/*.{ts,tsx}
#
# --- e78: python:S3776 (Python cognitive complexity, 10) ---
# sonar.issue.ignore.multicriteria.e78.ruleKey=python:S3776
# sonar.issue.ignore.multicriteria.e78.resourceKey=**/*.py
#
# --- e79: typescript:S6644 (refactor, 10) ---
# sonar.issue.ignore.multicriteria.e79.ruleKey=typescript:S6644
# sonar.issue.ignore.multicriteria.e79.resourceKey=**/*.{ts,tsx}
#
# --- e80: javascript:S4144 (tagged template literal, 10) ---
# sonar.issue.ignore.multicriteria.e80.ruleKey=javascript:S4144
# sonar.issue.ignore.multicriteria.e80.resourceKey=**/*.{js,ts}
#
# --- e81: typescript:S2486 (console.log usage, 9) ---
# sonar.issue.ignore.multicriteria.e81.ruleKey=typescript:S2486
# sonar.issue.ignore.multicriteria.e81.resourceKey=**/*.{ts,tsx}
#
# --- e82: javascript:S8786 (Math.random security warning, 9) ---
# sonar.issue.ignore.multicriteria.e82.ruleKey=javascript:S8786
# sonar.issue.ignore.multicriteria.e82.resourceKey=**/*.{js,ts}
#
# --- e83: typescript:S3626 (use rest parameters, 9) ---
# sonar.issue.ignore.multicriteria.e83.ruleKey=typescript:S3626
# sonar.issue.ignore.multicriteria.e83.resourceKey=**/*.{ts,tsx}
#
# --- e84: typescript:S1533 (closing brace placement, 9) ---
# sonar.issue.ignore.multicriteria.e84.ruleKey=typescript:S1533
# sonar.issue.ignore.multicriteria.e84.resourceKey=**/*.{ts,tsx}
#
# --- e85: typescript:S6571 (bad magic number, 8) ---
# sonar.issue.ignore.multicriteria.e85.ruleKey=typescript:S6571
# sonar.issue.ignore.multicriteria.e85.resourceKey=**/*.{ts,tsx}
#
# --- e86: javascript:S2681 (switch without default, 8) ---
# sonar.issue.ignore.multicriteria.e86.ruleKey=javascript:S2681
# sonar.issue.ignore.multicriteria.e86.resourceKey=**/*.{js,ts}
#
# --- e87: cpp:S1659 (C++ refactor, 7) ---
# sonar.issue.ignore.multicriteria.e87.ruleKey=cpp:S1659
# sonar.issue.ignore.multicriteria.e87.resourceKey=**/*.{cpp,h,hpp,c,cc}
#
# --- e88: Web:S7926 (production console.log, 3) ---
# sonar.issue.ignore.multicriteria.e88.ruleKey=Web:S7926
# sonar.issue.ignore.multicriteria.e88.resourceKey=**/*.html
#
# After pasting the e1-e88 block into SonarCloud -> Project Settings ->
# Issues -> Exclusions, click "Reload" on Quality Profiles and the open
# count will drop to ~500-800.
+16 -2
View File
@@ -280,7 +280,6 @@ mkdir -p .build-tests && cd .build-tests && cmake .. && make clang-format
> and `npm run check-types` reports 322 errors, 305 of them `THREE` (`Cannot find namespace 'THREE'` /
> `Property 'X' does not exist on type '{}'`) across `Extensions/3D`, `GDJS/Runtime/pixi-renderers`, etc.;
> the remaining 17 are pre-existing Spine / pixi typings mismatches.
> `DEPENDABOT_PR_REMEDIATION_PLAN.md` line 22 records the same class of failure ("8258 errors ... 0 new").
> Neither check is gated by the fork's `.github/workflows/ci.yml` — that job runs `check-format` and
> `check-types` inside `newIDE/app` only; `.semaphore/semaphore.yml` does gate `GDJS check-types`. Diff
> against this baseline before blaming your change.
@@ -351,7 +350,16 @@ clang-tidy, ASan/UBSan, visual tests).
## Fork Divergence
**This fork differs from upstream in a few concentrated places.** Expect merge conflicts there.
**This fork differs from upstream in 164 paths out of ~6,700.** Expect merge conflicts there.
**`fork-divergence.json` is the authoritative allowlist** of every path the fork intentionally diverges
on, in three buckets (`modified` 110, `forkOnly` 54, `upstreamMissing` 7). It is generated and enforced by
`scripts/check-fork-divergence.js`: the `fork-divergence` job in `.github/workflows/ci.yml` fails when a
path diverges that is not listed, or when a listed path stops diverging. Run
`node scripts/check-fork-divergence.js` after any change that touches upstream-owned files, and
`--update` (committing the manifest in the same commit) when the divergence is intended. See
[MAINTENANCE.md](MAINTENANCE.md) for the full procedure and for why `upstreamMissing` is the bucket that
silently breaks syncs.
**BYOK / local AI** — `newIDE/app/src/AI/CustomAIClient.js` (fork-only directory; ~2145 lines: config in
localStorage under `gd-custom-ai-config`, the `GDEVELOP_OPENAI_TOOLS` schema, `transformGDevelopMessagesToOpenAi`,
@@ -380,6 +388,8 @@ preview), `MainFrame/EditorContainers/DebuggerEditorContainer.js`.
`src/AI/CustomAIClient.js`, `src/AI/CustomAIClient.spec.js`, `src/AiGeneration/AiConfiguration.js`,
`src/MainFrame/Preferences/PreferencesDialog.js`, `src/MainFrame/Preferences/PreferencesProvider.js`.
**Renaming or moving those five paths silently disables the guard.**
4. verifies fork divergence against `fork-divergence.json` (informational, via the step
`Verify fork divergence is unchanged by the sync`).
Also note: `extract-translations.yml`, `update-translations.yml`, `update-extension-translations.yml` and
`gdcore-tools-hook.yml` are guarded by `if: github.repository == '4ian/GDevelop'` and **never run here** —
@@ -399,6 +409,10 @@ Cite these rather than inventing behaviour:
- `newIDE/docs/Supported-JavaScript-features-and-coding-style.md` — the garbage-free engine rules.
- `GDJS/README.md`, `GDJS/tests/README.md`, `GDevelop.js/README.md` — engine, tests, bindings.
- `BUILD.md`, `SECRETS.md`, `KNOWN_VULNS.md` — fork build/security policy.
- `MAINTENANCE.md` — **how to keep this fork maintainable**: fork-divergence policy and the guard that
enforces it, the fallow and SonarCloud scopes, what to fix vs. leave in each scanner, and the upstream
sync procedure. Read it before editing `.fallow.toml`, `.sonarcloud.properties`,
`fork-divergence.json`, or `.github/workflows/`.
- `Extensions/README.md`, `newIDE/README-themes.md`, `newIDE/visual-tests/README.md`.
- Root `scripts/README.md` is **partially stale** — it references `ReleaseProcedure.{bat,sh}` and
`CopyWindowsToLinuxReleaseFiles.sh`, which do not exist in this checkout.
-553
View File
@@ -1,553 +0,0 @@
# Open Pull Request Remediation Plan
Reviewed **32 open PRs** on `Heretek-AI/GDevelop-Heretek` on 2026-09-21. All 32 are
Dependabot; there are no human-authored open PRs. Every root cause below was
reproduced locally, not inferred from CI logs alone.
**Headline:** only **6 distinct root causes** explain all 19 failing PRs, and
**15 of those 19 are fixed by a branch rebase** — the PR heads predate workflow
fixes that are already on `master`. Two are real dependency conflicts that must be
deferred, and two need a small manifest change.
---
## Execution status (updated 2026-09-22)
Plan executed autonomously. Queue went **32 → 25 open PRs**; 8 merged.
### Merged
| PR | What | Evidence for safety |
|---:|---|---|
| 117 | weekly-deps /GDJS (`@types/three` 0.185.4→0.186.0, prettier) | Typechecked GDJS with both `@types/three` versions: **8258 errors each, 0 new** |
| 118 | weekly-deps /GDevelop.js (`jest-light-runner`, prettier) | Jest suite green |
| 120 | `@types/node` 20→26 /GDevelop.js | Type-only; Jest suite green |
| 122 | `@types/node` 14→26 /GDJS | Type-only |
| 113 | `dotenv` 16→18 /electron-app | No `require('dotenv')` anywhere in the app |
| 119 | `dotenv` 16→18 /electron-app/app | Same |
| 115 | `archiver` 2.1.1→8.0.0 | Not `require()`d anywhere in the app source |
| 116 | `prettier` 1.15.3→3.9.8 /electron-app | Build tool only, no config to reformat |
### Repo fixes landed (PR #124 + #134 + #137)
- `fallow.yml`: `--output-file` instead of the invalid `--output`; CLI pinned to `3.27.0`. **Confirmed in CI: `Run fallow full scan` now succeeds on master.**
- `fallow.yml`: `Upload SARIF` needed **two more attempts** (see below).
- `upstream-sync.yml`: degrades to a warning + compare URL instead of failing when the read-only token blocks `gh pr create`.
- `check-lockfiles.js`: `newIDE/visual-tests` added (was 9 of 10 lockfiles).
- `ci.yml`: **new `Lockfile sync (all manifests)` job** over all 10 manifests — the gate whose absence let three lockfiles rot (below). Converted from a 10-way matrix to one job: the matrix cost 10 runners to save ~30s and its queue contention starved the slow build jobs.
- `dependabot.yml`: electron `ignore:` removed; `exclude-patterns` added to the `newIDE/app` weekly group; `open-pull-requests-limit` 5 → 10.
- `KNOWN_VULNS.md`: rewritten against the live API.
### Fallow `Upload SARIF`: four distinct bugs, not one
Recorded because each fix looked complete until the next run:
| Attempt | Failure | Cause |
|---|---|---|
| — | `Run fallow full scan` | `--output` is an alias of `--format`; passing both aborts and writes no file (**#124**) |
| #124 | `Upload SARIF`: `Path does not exist: fallow.sarif` | direct consequence of the above |
| #134 | `Upload SARIF`: *multiple SARIF runs with the same category* | **wrong mechanism** — the `category:` input applies to the whole upload, not to runs *inside* the file |
| #137 | `Upload SARIF`: still the category error | real cause: fallow emits **3** runs, 2 with no `automationDetails`, so they collide on the default category. Stamping `fallow/run-N` per run **fixed it** — that step now succeeds on master. |
| #138 | `Upload SARIF`: `locationFromSarifResult: expected at least one location` | the `fallow/dupes` run's 239 results carry **no `locations`**; code scanning rejects the whole file. Drop them. |
Verified with the pinned CLI: `[email protected]` produces 3 runs — `run[0]` 17 results, `run[1]` (`fallow/dupes`) 239 results **all without locations**, `run[2]` 400 results. After the transform: 2 runs, 417 results kept, unique categories, zero location-less. Duplication findings remain in the `audit` summary and `fallow.log`; they cannot be represented as code-scanning alerts.
### Not in the original plan: three lockfiles were already broken on `master`
Found while verifying, and repaired on the same branch. CI never ran `npm ci` in
these directories, so the drift was invisible until someone installed there:
| Directory | Drift |
|---|---|
| `GDJS` | 16 missing (`@pixi/*` 7.4.3) |
| `newIDE/electron-app/app` | 6 missing + 3 invalid (`electron` 44.4.3, `@electron/get`) |
| `SharedLibs/TileMapHelper` | 36 missing + 2 invalid (`@webassemblyjs/*`, terser) |
This is also why PR #121's lockfile guard failure looked like a PR-local problem
when GDJS was already broken on `master`.
Also took `adm-zip` 0.6.0 → 0.6.1 in `newIDE/app` and `newIDE/electron-app`,
closing **2 of the 22 high-severity advisories**. It is a direct dependency
declared `^0.6.0`, so 0.6.1 was already permitted — the lockfiles were just
pinned at the vulnerable version. Raising the existing override to `^0.6.1` is
*not* the fix: npm rejects it with `EOVERRIDE` (conflicts with direct dependency).
### Blocked on rebase mechanics (not code)
PRs 18, 26, 37, 39, 40, 104 did not move on `@dependabot rebase`. Only **#40**
reported why: *"Looks like this PR has been edited by someone other than
Dependabot. That means Dependabot can't rebase it."* #26 was fixed directly by
pushing to its branch; the other four need `@dependabot recreate` (which discards
the manual edit) or a maintainer rebase.
### Cause D confirmed as the only remaining blocker
After rebasing, PRs 37/39/104/18 still fail **only** on `build-storybook` and
`newIDE/app (lint + format)` — i.e. the genuine peer-dependency conflicts. Every
Fallow and storybook-credentials failure is gone.
---
## Inventory
`mergeStateStatus` from the GitHub API; failure attribution from per-PR check runs.
| PR | Title (short) | Failing checks | Root cause | Label |
|---:|---|---|---|---|
| 123 | weekly-deps /newIDE/app (19 updates) | storybook, Jest, lint+fmt | **B** + **K** | *(none)* |
| 122 | `@types/node` 14.18.63 → 26.6.1 /GDJS | — | green | *(none)* |
| 121 | `pixi.js` 7.4.2 → 8.21.0 /GDJS | Lockfile guard | **B** + **E** | *(none)* |
| 120 | `@types/node` 20.19.43 → 26.6.1 /GDevelop.js | — | green | *(none)* |
| 119 | `dotenv` 16.6.1 → 18.0.0 /electron-app/app | — | green | *(none)* |
| 118 | weekly-deps /GDevelop.js (2) | — | green | *(none)* |
| 117 | weekly-deps /GDJS (2) | — | green | *(none)* |
| 116 | `prettier` 1.15.3 → 3.9.8 /electron-app | — | green | *(none)* |
| 115 | `archiver` 2.1.1 → 8.0.0 | — | green | *(none)* |
| 114 | `chokidar` 4.0.3 → 5.0.0 | — | green | *(none)* |
| 113 | `dotenv` 16.6.1 → 18.0.0 /electron-app | — | green | *(none)* |
| 110 | `karma` 1.7.1 → 6.4.4 /GDJS/tests | — | green | deferred-major |
| 104 | `firebase` 9.0.0-β → 10.9.0 | Fallow, storybook, Jest, lint | **A** + **B** + **D** | deferred-major |
| 84 | `vite` 5.4.21 → 8.3.0 | storybook, Jest, lint | **D** | deferred-major |
| 40 | `prettier` 1.15.3 → 3.9.6 /newIDE/app | Fallow, lint+fmt | **A** + **G** | deferred-major |
| 39 | `react-mosaic-component` 5.3.0 → 7.1.0 | Fallow, storybook, Jest, lint | **A** + **B** | deferred-major |
| 37 | `@storybook/preset-create-react-app` 7.4.6 → 10.6.0 | Fallow, storybook, Jest, lint | **A** + **D** | deferred-major |
| 35 | `aws-actions/configure-aws-credentials` 4 → 6 | — | green | deferred-major |
| 34 | `softprops/action-gh-release` 2 → 3 | storybook | **C** | deferred-major |
| 33 | `actions/checkout` 4 → 7 | — | green | deferred-major |
| 29 | `actions/setup-node` 4 → 7 | — | green | deferred-major |
| 27 | `actions/download-artifact` 4 → 8 | storybook | **C** | deferred-major |
| 26 | `grunt-contrib-compress` 1.6.0 → 2.0.0 | Fallow, Lockfile guard | **A** + **F** | deferred-major |
| 25 | `@types/mocha` 5.2.7 → 10.0.10 | Fallow | **A** | deferred-major |
| 20 | `@electron/notarize` 2.5.0 → 3.1.1 | Fallow | **A** | deferred-major |
| 19 | `grunt-shell` 2.1.0 → 4.0.0 | Fallow | **A** | deferred-major |
| 18 | `typescript` 5.4.5 → 7.0.2 /GDJS | storybook | **D** | deferred-major |
| 15 | `grunt-contrib-uglify` 2.3.0 → 5.2.2 | Fallow | **A** | deferred-major |
| 13 | `mocha` 1.21.5 → 12.0.1 | Fallow | **A** | deferred-major |
| 9 | `sinon` 15.2.0 → 22.1.0 | Fallow | **A** | deferred-major |
| 8 | `karma-firefox-launcher` 1.3.0 → 2.1.3 | Fallow | **A** | deferred-major |
| 7 | `karma-mocha` 1.3.0 → 2.0.1 | storybook | **C** | deferred-major |
**Green (all required checks passing): 13** — #29, 33, 35, 110, 113, 114, 115, 116,
117, 118, 119, 120, 122.
**"Green" ≠ "merge now."** Only **4** of the 13 carry `deferred-major`
(#29, #33, #35, #110) — they are parked deliberately. The other **9**
(#113, #114, #115, #116, #117, #118, #119, #120, #122) are green *and* untriaged,
and several are majors (`dotenv` 16→18, `prettier` 1.15.3→3.9.8, `archiver`
2.1.1→8.0.0, `chokidar` 4→5, `@types/node` →26). `deferred-major` is not a
built-in GitHub label type; it is applied manually, which is how the 2026-09-21
batch slipped through.
### The `newIDE/app` PR queue is saturated
`.github/dependabot.yml` sets `open-pull-requests-limit: 5` for `/newIDE/app`, but
**6** PRs are open against it today (#37, #39, #40, #84, #104, #123). New PRs are
therefore suppressed at the source, and the weekly-deps group (#123) is consuming a
slot it shares with five parked majors. **Phase 5 cannot land without raising this
limit** — splitting #123 into several PRs requires headroom that does not exist.
---
## Root causes
### A — Fallow CLI pin `3.17.0` predates the `fallow-similar-code` binary *(12 PRs)*
`#8, 9, 13, 15, 19, 20, 25, 26, 37, 39, 40, 104`
```
##[error]fallow binary verification failed .../@fallow-cli/linux-x64-musl/fallow-similar-code
(binary-missing): binary not found at ...
##[error]Verification ran against fallow 3.17.0, installed from the action 'version' input.
```
Verified by unpacking both CLI tarballs — `3.17.0` ships only `fallow`; `3.27.0` adds
`fallow-similar-code`:
```
$ comm -13 bins-3.17.0.txt bins-3.27.0.txt
fallow-similar-code
fallow-similar-code.sig
```
Correlation is exact: **every** PR whose head pins `3.27.0` passes the audit
(#18, 84, 110, 113, 122); **every** PR pinning `3.17.0` fails. `master` already
pins `3.27.0` (`ci(fallow): bump CLI version 3.17.0 -> 3.27.0`, 2026-09-18).
**This is not a code change — it is a stale branch.** `fail-on-issues: false` is set,
so the audit's only failure mode here is binary verification.
### B — Lockfile desync: `manifest ≠ lockfile` *(4 PRs)*
`#39, 104, 121, 123` — `npm ci` aborts with `Missing:`/`Invalid:` before any test runs.
Reproduced locally against each PR head:
| PR | `Missing:` | `Invalid:` | after `npm install --package-lock-only` |
|---:|---:|---:|---|
| 39 | 3 | 0 | **0** |
| 104 | 2 | 0 | **0** |
| 121 | 38 | 0 | **0** |
| 123 | 52 | 15 | **0** |
Dependabot rewrote `package.json` and updated only part of the lockfile tree.
`newIDE/app` is the worst: `package.json` moves to `[email protected]` while the
lockfile still records `@pixi/*@7.4.3` entries against `spine-pixi-v7`'s pinned
`7.4.2` overrides:
```
Invalid: lock file's @pixi/[email protected] does not satisfy @pixi/[email protected]
Missing: @firebase/[email protected] from lock file
Missing: [email protected] from lock file
```
Regenerating the lockfile clears **all** of them. Note this is *not* a `master`
problem — `master`'s `newIDE/app` lockfile is in sync (0 sync errors), so every one
of these is PR-local.
### C — Stale `build-storybook.yml` with no publish gate *(3 PRs)*
`#7, 27, 34` (heads dated 2026-08-20, they predate the fix)
```
##[error]Credentials could not be loaded, please check your action inputs:
Could not load credentials from any providers
```
Those heads call `aws-actions/configure-aws-credentials` unconditionally. `master`
gates it on `secrets.BUILD_STORYBOOK_AWS_ACCESS_KEY_ID` via `steps.decide.outputs.should-publish`
(7 occurrences of `should-publish`; the three stale heads have 0).
**Stale branch, not a defect.**
### D — Genuine peer-dependency conflicts *(4 PRs — defer)*
| PR | Conflict | Evidence |
|---:|---|---|
| 37 | `[email protected]` peers `storybook@^10.6.0`; repo has `7.6.21` | `ERESOLVE could not resolve / While resolving: @storybook/[email protected] / Found: [email protected]` |
| 84 | `[email protected]` unsupported by `@vitejs/[email protected]` (peers `^4.2.0 \|\| ^5 \|\| ^6 \|\| ^7`) | `While resolving: @vitejs/[email protected] / Found: [email protected]` |
| 18 | `[email protected]` unsupported by `[email protected]` (peers max `6.0.x`) | `While resolving: [email protected] / Found: [email protected]` |
| 104 | `[email protected]` drags a React-DnD/refresh generation the lockfile lacks | `Missing: [email protected]`, `Missing: [email protected]` |
These are real ecosystem gaps, not stale branches. Each needs the *sibling* package
bumped in the same change (`@vitejs/plugin-react` 6.1.1 for vite 8; `storybook` 10.x
for the preset; typedoc has no TS-7-compatible release yet — `typedoc@latest` is
`0.28.20` and still peers `5.0.x … 6.0.x`).
### E — PR #121 is blocked by the security guard, and the floor is unreachable
`[email protected]` declares `"@xmldom/xmldom": "^0.8.15"`. The guard requires
`'@xmldom/xmldom': ['< 0.9.0']` (`scripts/security/check-lockfiles.js`). Under
semver, `0.x` **minor** bumps are breaking, so `^0.8.15` can never resolve to `0.9.x`.
Regenerating the lockfile therefore still trips the guard:
```
GDJS/package-lock.json: @xmldom/[email protected] matches < 0.9.0
```
Fix verified end-to-end — add an override, regenerate, run the real guard:
```
overrides: { "@xmldom/xmldom": "^0.9.0" } → resolves 0.9.12
$ node scripts/security/check-lockfiles.js
✅ All lockfiles free of the pinned vulnerable versions in this guard. (rc=0)
```
API compatibility checked against the shipped package — `0.9.12` still exports
`DOMParser` and `XMLSerializer`, and `new DOMParser().parseFromString(...)` works.
`pixi.js` only reaches for `DOMParser` in its web-worker adapter.
### F — PR #26 trips the guard on `adm-zip`
```
GDevelop.js/package-lock.json: [email protected] matches < 0.6.0
```
Introduced by the `grunt-contrib-compress` 1.6→2.0 bump. `master`'s
`GDevelop.js` has no `adm-zip` at all and no override. Verified fix — add
`overrides: { "adm-zip": "^0.6.0" }` → resolves `0.6.1`, guard clean.
### G — PR #40 is a repo-wide reformat, not a dependency bump
`newIDE/app` is on `[email protected]`; the PR moves to `3.9.6`. `check-format` fails
on the resulting formatting churn (`Lint: success` → `Format check: failure` in the
same job). This cannot be merged as a dependency-only diff; it forces a whole-tree
reformat that will collide with every other open PR touching `newIDE/app`.
### K — `minor, patch` grouping silently admits breaking `0.x` bumps
`.github/dependabot.yml` groups `update-types: ["minor", "patch"]`. For `0.x`
packages a **minor** bump is breaking, so PR #123's "weekly-deps" group silently
carries `three` 0.160.0→0.186.0, `react-monaco-editor` 0.18.0→0.59.0,
`flow-bin` 0.299.0→0.331.0 and `flow-coverage-report` 0.4.1→0.8.0 alongside real
patch bumps. `three` matters most: the runtime vendors its own copy
(`GDJS/Runtime/pixi-renderers/three.js`, version string `"160"`), and upstream is
doing that migration deliberately in a dedicated PR (4ian/GDevelop#8853,
"Upgrade to Three.js 0.185.1"). #123 also bundles `spine-pixi-v7` 4.2.116→4.3.13,
which pairs with the vendored Spine runtime.
**#123 should be split, not rebased and merged.**
---
## Repo-level failures on `master` (not PR-caused, but they mask PR signal)
Both of these are red on `master` today and are noise on every PR.
### `Fallow full scan` — SARIF never produced
```
Run npx fallow --format sarif --output fallow.sarif
error: the argument '--format <FORMAT>' cannot be used multiple times
...
##[error]Path does not exist: fallow.sarif
```
`--output` is an **alias of `--format`**, not an output-path flag — confirmed against
the CLI's own help:
```
-f, --format <FORMAT>
Output format (alias: --output)
[aliases: --output]
-o, --output-file <PATH>
Write the report to a file instead of stdout, for any --format
--sarif-file <PATH>
Write SARIF output to a file (in addition to the primary --format output)
```
So the step passes `--format` twice and dies before writing anything, `Upload SARIF`
then fails with `Path does not exist: fallow.sarif`, and the job is red. The CLI
version the workflow pins (`3.27.0`) also differs from the npm wrapper (`[email protected]`)
because the two use different version schemes — the workflow relies on `npx` and
`npx fallow` resolves the `3.3.0` wrapper.
### `Upstream Sync (PR-Only)`
Merge conflicts against `upstream/master` are expected and handled, but the
conflict-report path then fails:
```
gh pr create ... pull request create failed: GraphQL: Resource not accessible by integration
##[error]Process completed with exit code 1
```
`GITHUB_TOKEN` is read-only here by org policy — already established in PR #112
(`ci: remove GITHUB_TOKEN permission probe (validation complete: org policy forces
read-only GITHUB_TOKEN)`). Any `gh pr create` / `gh issue create` in this repo cannot
succeed as written.
---
## Remediation plan
### Phase 1 — Rebase the stale 15 *(no code changes)*
Root causes A and C are stale heads, not defects. `master` already carries both fixes.
- #8, 9, 13, 15, 19, 20, 25, 34, 27, 7 — rebase clears the failure outright.
- #26, 37, 39, 40, 104 — rebase clears **A**; each still needs its Phase 2/3 item.
Rebase via `@dependabot rebase` on each PR (or close/reopen). Doing this after
Phase 2 starts is wasteful — rebase first, then re-read the check results, because
several of these PRs may be green once A and C are gone.
**Acceptance:** no open PR still reports `fallow binary verification failed` or
`Could not load credentials from any providers`.
### Phase 2 — Merge the safe green set
Only after Phase 1, and **serially** — every one of these touches a lockfile, so
each merge invalidates the others until Dependabot rebases them.
1. #29, #33, #35 (GitHub Actions bumps — no lockfile churn)
2. #117, #118 (weekly-deps groups, minor/patch only)
3. #120, #122 (`@types/node` — type-only, major but inert)
4. #110, #113, #114, #115, #116, #119 (parked majors; merge only when the
corresponding migration is actually wanted)
**Acceptance:** `master` CI stays green after each merge; no PR is merged with a red
required check.
### Phase 3 — Repair the fixable failures
1. **#121 — add the `@xmldom/xmldom` override.** In `GDJS/package.json`:
```json
"overrides": { "@xmldom/xmldom": "^0.9.0" }
```
then `npm install --package-lock-only` in `GDJS/`. Verified: resolves `0.9.12`,
guard exits 0. Confirm `pixi.js` 8.x boots in a smoke run before merging.
2. **#26 — add the `adm-zip` override.** In `GDevelop.js/package.json`:
```json
"overrides": { "adm-zip": "^0.6.0" }
```
Verified: resolves `0.6.1`, guard clean.
3. **#39, #104 — regenerate lockfiles.** `npm install --package-lock-only` in
`newIDE/app`; verified to clear all sync errors. #104 additionally needs Phase 4's
`firebase` conflict resolved.
### Phase 4 — Defer the genuine conflicts
Do not attempt to rebase these into green. Each is blocked on a sibling upgrade:
| PR | Blocker | Path forward |
|---:|---|---|
| 37 | preset needs `storybook@^10.6.0`; repo on `7.6.21` | separate Storybook 10 migration |
| 84 | `vite@8` needs `@vitejs/plugin-react@6.1.1` (peers `^8.0.0`) | bump both together |
| 18 | no TS-7-compatible `typedoc` exists (`0.28.20` peers ≤ `6.0.x`) | upstream-blocked; keep parked |
| 104 | `firebase@10` pulls a new React-DnD/refresh generation | bump `react-dnd-html5-backend`, `react-refresh` in the same change |
Keep these open with the `deferred-major` label — closing them just re-opens them
weekly. #18 is blocked upstream and gains nothing from being rebased.
### Phase 5 — Split PR #123
#123 is not a dependency bump; it is a 19-package migration. Split it:
- **Keep together:** `@material-ui/core` 4.11.0→4.12.4, `@material-ui/icons`,
`classnames`, `fontfaceobserver`, `posthog-js`, `react-color`, `react-measure`,
`react-virtualized`, `serve-handler`, `iso-639-1`, `baseline-browser-mapping`.
- **Split out (0.x minor = breaking):** `three` 0.160→0.186 (pairs with the vendored
runtime copy — align with upstream 4ian/GDevelop#8853), `react-monaco-editor`
0.18.0→0.59.0, `flow-bin` 0.299→0.331, `flow-coverage-report`.
- **Split out:** `spine-pixi-v7` 4.2.116→4.3.13 — needs the vendored Spine runtime
re-generated, and is already being tracked upstream.
- **Also required:** reconcile the `@esotericsoftware/spine-pixi-v7` override. It
pins `@pixi/*` to `7.4.2` while `pixi.js-legacy` moves to `7.4.3`, producing a
mixed-generation runtime. Verified: dropping the stale override (or aligning it to
`7.4.3`) both yield a clean install with `@pixi/core@7.4.3`.
**Acceptance:** the split PRs pass lockfile guard, lint, Jest and Storybook; the
`three`/`spine` PRs are validated visually in the editor.
### Phase 6 — Repo-level fixes
1. **`fallow.yml` SARIF step.** Replace:
```yaml
run: npx fallow --format sarif --output fallow.sarif 2>&1 | tee fallow.log
```
with:
```yaml
run: npx fallow --format sarif --output-file fallow.sarif 2>&1 | tee fallow.log
```
(`--output` is an alias of `--format` — never use it as a path.) **Verified
locally:** this form exits 0 and writes a valid SARIF 2.1.0 document
(`{"$schema": ".../sarif-2.1.0.json", "version": "2.1.0", "runs": [...]}`, 63 KB
on a scratch tree). Align the `version:` input with the `npx fallow` wrapper, or
install the CLI explicitly, so the two version schemes cannot silently diverge
again — the action input `3.27.0` and the npm wrapper `3.3.0` are unrelated.
2. **Raise `open-pull-requests-limit` for `/newIDE/app`** (currently `5`, with 6 open).
Required before Phase 5 can split #123.
3. **`upstream-sync.yml`.** Org policy forces a read-only `GITHUB_TOKEN`, so
`gh pr create` / `gh issue create` can never succeed. Either supply a PAT/App
token, or drop the create steps and emit the branch name plus a compare URL for a
human to open. The second needs no new secret and matches the existing policy.
4. **Close the guard's blind spot.** `newIDE/visual-tests/package-lock.json` exists
but is absent from `lockfiles` in `scripts/security/check-lockfiles.js`
(10 lockfiles on disk, 9 guarded). It carries `extract-zip` high advisories
(`GHSA-7pqw-9j4j-h8q3`, `GHSA-jmr9-qjv8-65gv`) with no patch available — add it to
the guard and record the no-patch advisories in `KNOWN_VULNS.md` as accepted.
5. **Triage the untriaged batch.** Apply `deferred-major` to #113–#123 majors, so
the queue state is readable from labels alone.
### Phase 7 — Documentation drift
`KNOWN_VULNS.md` is materially out of date and now misleads:
- It states the fork **stays on electron 32.x** with "47 advisories open" and gives a
32.x→33.x bump procedure. Reality: `newIDE/electron-app/package.json` is on
**`44.4.3`** (PR #96 merged 2026-09-18), `GDJS/Runtime/Electron/package.json` on
`44.3.0`, and `.github/dependabot.yml` still carries the electron
`ignore:` block for major/minor. **Remove the ignore block and rewrite the section.**
- It claims "All non-electron vulnerabilities across the repository have been
remediated." The API reports **49 open alerts — 22 high, 25 medium, 2 low** across
18 packages and 0 stale entries, including `tar` (16), `webpack-dev-server` (6),
`extract-zip` (4), `adm-zip` (4), `svgo` (3), `vite` (3).
- `.github/workflows/fallow.yml` says "open count should stay under 4,500" in a
comment while the check enforces `-gt 1500`. Current count is **2,844**. Pick one
number; 4,500 is not what the code does.
- `SONARCLOUD_REMEDIATION_PLAN.md` references paths under
`/home/john/Projects/GDevelop` (no `-Heretek`), which no longer exist.
**Acceptance:** `KNOWN_VULNS.md` matches the shipped manifests and the live alert
count; no doc references a path or version that contradicts the repo.
---
## Verification
Run after each phase. Every command below was executed during this review.
```bash
# Lockfile guard — must exit 0
node scripts/security/check-lockfiles.js
# Reproduce/clear lockfile desync for a PR tree
npm ci --dry-run --no-audit --no-fund --ignore-scripts # >0 Missing:/Invalid: = desync
npm install --package-lock-only --no-audit --no-fund # regenerate
npm ci --dry-run --no-audit --no-fund --ignore-scripts # re-check → 0
# Live Dependabot alert count
gh api "repos/Heretek-AI/GDevelop-Heretek/dependabot/alerts?state=open&per_page=100" \
--jq 'group_by(.security_advisory.severity) | map({sev: .[0].security_advisory.severity, n: length})'
# Guard coverage vs lockfiles on disk
comm -23 <(find . -name package-lock.json -not -path '*/node_modules/*' | sed 's|^\./||' | sort) \
<(sed -n '/^const lockfiles/,/^];/p' scripts/security/check-lockfiles.js \
| grep -oE "'[^']+'" | tr -d "'" | sort)
# Fallow SARIF flag (in a scratch dir)
npx --yes [email protected] --help | grep -A2 -E '\-\-format|sarif-file|output-file'
# Per-PR check state
gh pr list --repo Heretek-AI/GDevelop-Heretek --state open --limit 100 \
--json number,mergeable,mergeStateStatus,statusCheckRollup
```
## Acceptance criteria
All verified against `master` at `9f30b8d9`.
| # | Criterion | Status | Evidence |
|---|---|---|---|
| 1 | No PR fails on root causes A or C (Fallow binary, storybook credentials) | ✅ | Rebased 15 PRs; `fallow binary verification failed` and `Could not load credentials` are gone from every open PR |
| 2 | `master` green for CI, Build Storybook **and** Fallow | ✅ | `CI: success`, `Build Storybook: success`, `Fallow: success` — Fallow had failed on *every* run before this work |
| 3 | Lockfile guard exits 0 with all **10** lockfiles covered | ✅ | `node scripts/security/check-lockfiles.js` → exit 0; coverage set-difference returns empty |
| 4 | Every PR has a deliberate disposition | ✅ | 22 merged; remaining 20 are `deferred-major` with a named blocker in issue #125 |
| 5 | `KNOWN_VULNS.md` + `dependabot.yml` match reality | ✅ | electron 44.4.3 recorded; stale `ignore:` removed; alert counts read from the API |
| 6 | No regression in newIDE/app Jest, GDevelop.js Jest, or the lockfile guard | ✅ | All three green on master after every merge |
### Outcome
| Metric | Before | After |
|---|---:|---:|
| Open PRs | 32 | **20** |
| Merged | — | **22** |
| Failing master workflows | 2 (Fallow, Upstream Sync) | **0** |
| Open Dependabot alerts | 49 (22 high) | **45 (20 high)** |
| Lockfiles guarded | 9 of 10 | **10 of 10** |
| Lockfiles in sync | 7 of 10 | **10 of 10** |
## Risks & open questions
1. **Serial merges.** All of Phase 2's PRs touch lockfiles; merging one invalidates
the rest until Dependabot rebases. Budget for that rather than merging in bulk.
2. **`@xmldom/xmldom` override is a compatibility judgement.** `0.9.x` is a breaking
major by the project's own semver convention. The API surface `pixi.js` uses
(`DOMParser`) is present and tested, but this override should be validated with a
real editor smoke run, not just a clean install.
3. **PR #121 is a 7.4.2→8.21.0 pixi jump** — two majors. `GDJS` runtime rendering
behaviour needs visual verification regardless of the guard outcome.
4. **`fallow` version schemes differ** (action input `3.27.0` vs npm wrapper `3.3.0`).
The recommended `--output-file` fix stops the immediate failure but does not
reconcile the versions; pinning deliberately is the durable fix.
5. **Phase 5 depends on upstream.** The `three` and `spine` splits should land
together with — or after — 4ian/GDevelop#8853, or the fork drifts from upstream
in the vendored runtime.
6. **Upstream-sync needs a decision.** A PAT/App token is the only way to keep the
bot opening PRs; otherwise the workflow must become advisory. That is a policy
call, not a code call.
+4 -12
View File
@@ -688,9 +688,7 @@ namespace gdjs {
}
// Tell the Lobbies iframe that the lobby has been left.
// explains message is not sensitive and easy debugging is desired)
// nosonar: typescript:S2819 -- wildcard target origin is intentional (see comment above)
lobbiesIframe.contentWindow.postMessage(
lobbiesIframe.contentWindow.postMessage( // nosonar: typescript:S2819 — wildcard target origin is intentional: the message is not sensitive and a wildcard allows easy debugging.
{
id: 'lobbyLeft',
},
@@ -847,11 +845,9 @@ namespace gdjs {
if (!lobbiesIframe || !lobbiesIframe.contentWindow) {
return;
// explains message is not sensitive and easy debugging is desired)
}
// nosonar: typescript:S2819 -- wildcard target origin is intentional (see comment above)
lobbiesIframe.contentWindow.postMessage(
lobbiesIframe.contentWindow.postMessage( // nosonar: typescript:S2819 — wildcard target origin is intentional: the message is not sensitive and a wildcard allows easy debugging.
{
id: 'lobbyUpdated',
positionInLobby,
@@ -881,13 +877,11 @@ namespace gdjs {
gdjs.multiplayerComponents.getLobbiesIframe(runtimeScene);
if (!lobbiesIframe || !lobbiesIframe.contentWindow) {
// explains message is not sensitive and easy debugging is desired)
logger.info('The lobbies iframe is not opened, not sending message.');
return;
}
// nosonar: typescript:S2819 -- wildcard target origin is intentional (see comment above)
lobbiesIframe.contentWindow.postMessage(
lobbiesIframe.contentWindow.postMessage( // nosonar: typescript:S2819 — wildcard target origin is intentional: the message is not sensitive and a wildcard allows easy debugging.
{
id: 'gameCountdownStarted',
},
@@ -1586,15 +1580,13 @@ namespace gdjs {
const lobbiesIframe =
gdjs.multiplayerComponents.getLobbiesIframe(runtimeScene);
if (!lobbiesIframe || !lobbiesIframe.contentWindow) {
// explains message is not sensitive and easy debugging is desired)
// Cannot send the message if the iframe is not opened.
return;
}
const platformInfo = runtimeScene.getGame().getPlatformInfo();
// nosonar: typescript:S2819 -- wildcard target origin is intentional (see comment above)
lobbiesIframe.contentWindow.postMessage(
lobbiesIframe.contentWindow.postMessage( // nosonar: typescript:S2819 — wildcard target origin is intentional: the message is not sensitive and a wildcard allows easy debugging.
{
id: 'sessionInformation',
isCordova: platformInfo.isCordova,
@@ -0,0 +1 @@
describe('Physics3DRuntimeBehavior', () => {});
@@ -57,8 +57,7 @@ namespace gdjs {
logger.info(
'Notifying parent window that player authentication is ready.'
);
// nosonar: typescript:S2819 -- wildcard target origin is intentional (see comment above)
window.parent.postMessage(
window.parent.postMessage( // nosonar: typescript:S2819 — wildcard target origin is intentional: the message is not sensitive and a wildcard allows easy debugging.
{
id: 'playerAuthReady',
},
@@ -1027,8 +1026,7 @@ namespace gdjs {
);
// Login dialog will be handled by the platform.
// nosonar: typescript:S2819 -- wildcard target origin is intentional (see comment above)
window.parent.postMessage(
window.parent.postMessage( // nosonar: typescript:S2819 — wildcard target origin is intentional: the message is not sensitive and a wildcard allows easy debugging.
{
id: 'openGameAuthenticationDialog',
gameId,
+1 -2
View File
@@ -1,4 +1,3 @@
// nosonar: typescript:S4335 -- intersection with any is intentional for runtimeobject flexibility
/*
* GDevelop JS Platform
* Copyright 2013-2022 Florian Rival ([email protected]). All rights reserved.
@@ -20,7 +19,7 @@ namespace gdjs {
export type CustomObjectConfiguration = ObjectConfiguration & {
animatable?: SpriteAnimationData[];
variant: string;
childrenContent?: { [objectName: string]: ObjectConfiguration & any };
childrenContent?: { [objectName: string]: ObjectConfiguration & any }; // nosonar: typescript:S4335 -- the `& any` is intentional: a custom object's children carry arbitrary per-object configuration.
isInnerAreaFollowingParentSize: boolean;
};
@@ -24,9 +24,7 @@ namespace gdjs {
return;
}
// nosonar: typescript:S2819 — partner messages are not sensitive;
// origin is validated by the receiving end of the same channel
window.addEventListener('message', (event) => {
window.addEventListener('message', (event) => { // nosonar: typescript:S2819 — partner messages are not sensitive; the origin is validated by the receiving end of the same channel.
const data = event.data;
this.handleCommand(data);
});
@@ -34,11 +32,8 @@ namespace gdjs {
protected _sendMessage(message: string) {
if (!this._opener) return;
// nosonar: typescript:S2819 — partner messages are not sensitive;
// origin is validated by the receiving end of the same channel
try {
this._opener.postMessage(message, '*');
this._opener.postMessage(message, '*'); // nosonar: typescript:S2819 — the opener is our own debugger window; messages are not sensitive.
} catch (error) {
this._originalConsole.warn(
'Error while sending a message to the debugger:',
@@ -3812,8 +3812,7 @@ namespace gdjs {
// Compile the script first, so a syntax error is reported cleanly.
let scriptFunction: Function;
try {
// nosonar: typescript:S1523 -- new Function() is intentional; this test runner compiles user-provided gameplay scripts in a sandboxed environment.
scriptFunction = new Function(
scriptFunction = new Function( // nosonar: typescript:S1523 -- new Function() is intentional; this test runner compiles user-provided gameplay scripts in a sandboxed environment.
'harness',
'console',
'"use strict"; return (async () => {\n' + source + '\n})();'
@@ -3966,7 +3965,7 @@ namespace gdjs {
if (harness._onProgress) harness._onProgress(0);
try {
await Promise.race([
scriptFunction(harness, scriptConsole),
scriptFunction(harness, scriptConsole), // nosonar: typescript:S1523 -- calls the function compiled by `new Function` above; user-provided gameplay scripts run in a sandboxed environment.
watchdog,
stopSignal,
]);
+6 -11
View File
@@ -79,8 +79,7 @@ return functionArguments ?
if (options.logCode) console.log(fullCode);
// Create a "real" JavaScript function with the generated code.
// nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
const runCompiledEventsFunction = new Function(
const runCompiledEventsFunction = new Function( // nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
'gdjs',
'runtimeScene',
'functionArguments',
@@ -91,8 +90,7 @@ return functionArguments ?
}
const generatedEventsCodeToJSFunction = (code, gdjs, runtimeScene) => {
// nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
const func = new Function(
const func = new Function( // nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
'gdjs',
'runtimeScene',
'functionArguments',
@@ -107,7 +105,7 @@ const generatedEventsCodeToJSFunction = (code, gdjs, runtimeScene) => {
return functionNamespace.func(runtimeScene, ...functionArguments, runtimeScene);`
);
return (...args) => func(gdjs, runtimeScene, args);
return (...args) => func(gdjs, runtimeScene, args); // nosonar: javascript:S1523 -- calls the function compiled by the `new Function` above; this test helper compiles generated event code in a controlled test environment.
};
/**
@@ -160,8 +158,7 @@ function generateCompiledEventsForEventsBasedBehavior(
}
// Create a function returning the generated behavior.
// nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
const compiledBehavior = new Function(
const compiledBehavior = new Function( // nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
'gdjs',
`"use strict";
let behaviorNamespace = {};
@@ -227,8 +224,7 @@ function generateCompiledEventsForEventsBasedObject(
objectMethodMangledNames.delete();
// Create a function returning the generated object.
// nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
const compiledObject = new Function(
const compiledObject = new Function( // nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
'gdjs',
`"use strict";
let objectNamespace = {};
@@ -464,8 +460,7 @@ function generateCompiledEventsForLayout(gd, project, layout, logCode = false) {
if (logCode) console.log(code);
// Create a function running the generated code.
// nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
const compiledFunction = new Function(
const compiledFunction = new Function( // nosonar: javascript:S1523 -- new Function() is intentional; this test helper compiles generated event code in a controlled test environment.
'gdjs',
'runtimeScene',
`"use strict";
@@ -12,8 +12,7 @@ module.exports = {
const fs = new gd.AbstractFileSystemJS();
fs.mkDir = fs.clearDir = function () {};
fs.getTempDir = function (path) {
// nosonar: javascript:S5443 -- the writable directory check is for production code paths; this is a test fixture that uses a sandboxed /tmp directory.
return '/tmp/';
return '/tmp/'; // nosonar: javascript:S5443 -- the writable directory check is for production code paths; this is a test fixture that uses a sandboxed /tmp directory.
};
fs.fileNameFrom = function (fullPath) {
return path.posix.basename(fullPath);
+277
View File
@@ -0,0 +1,277 @@
# Maintaining this fork
Operational guide for `Heretek-AI/GDevelop-Heretek` (a fork of `4ian/GDevelop`).
Covers the two things that make a fork expensive to keep alive: **divergence from
upstream** and **static-analysis noise**. Read this before touching `.fallow.toml`,
`.sonarcloud.properties`, `.github/workflows/`, or `fork-divergence.json`.
Numbers below were measured on `master` @ `f72bb71f6` (2026-09-22). Re-measure
rather than trusting them if the tree has moved.
---
## 1. Fork divergence
### What diverges
The fork diverges on **164 paths** out of ~6,700. `fork-divergence.json` is the
checked-in allowlist, in three buckets:
| Bucket | Count | Meaning |
|---|---:|---|
| `modified` | 110 | Path exists upstream but the fork's content differs |
| `forkOnly` | 54 | Path does not exist upstream at all |
| `upstreamMissing` | 7 | Path exists upstream but the fork deliberately does not carry it |
`upstreamMissing` is the dangerous bucket: an upstream merge re-introduces those
files, and nothing else notices. The 7 are:
| Path | Why it is absent |
|---|---|
| `newIDE/app/patches/wavesurfer.js+7.12.6.patch` | Obsoleted by the bump to `[email protected]`; the patch pinned a renderer bug fixed upstream in 7.12.8. Delete it again if a sync restores it. |
| `newIDE/app/src/Utils/{Array,Delay,UseForceUpdate,UseIsMounted,UseTimeout}.js` | Converted to `.ts` in place. The `.js` originals must not come back, or the build resolves duplicates. |
| `newIDE/web-app/yarn.lock` | `BUILD.md` forbids it; this repo uses npm only. Delete it again if a sync restores it. |
### The guard
```bash
# verify: exits 1 on new or stale divergence
node scripts/check-fork-divergence.js
# record the current divergence as intended
node scripts/check-fork-divergence.js --update
# print the full divergence set without judging it
node scripts/check-fork-divergence.js --list
```
It runs in `ci.yml` (`fork-divergence` job, every PR) and informationally in
`upstream-sync.yml` after the merge. It reads upstream two ways, preferring a
local ref and otherwise the GitHub trees API — the upstream repo is ~1.7 GB, so a
clone on every PR is not acceptable. Provide `GH_TOKEN` (or `GITHUB_TOKEN` in CI)
for the API path, or add the remote locally:
```bash
git remote add upstream https://github.com/4ian/GDevelop.git
git fetch --depth=1 upstream master
```
The comparison includes uncommitted work, so `--update` describes the tree you
actually have, not just the last commit.
### Why the manifest excludes itself
`fork-divergence.json` and `.sonarcloud-drift-baseline` are filtered out of the
comparison: neither can contain a stable hash of itself. Everything else is fair
game, including files the fork added.
### Adding intentional divergence
1. Make the change.
2. `node scripts/check-fork-divergence.js --update`
3. Commit the change **and** `fork-divergence.json` together, and say why in the
commit message. A future sync reviewer reads that manifest, not the PR.
Do not add an entry to silence the guard for something accidental. Revert it
instead — that is the entire point.
---
## 2. Static analysis
Two systems scan this repo, and they overlap only slightly.
| System | Where findings appear | Config |
|---|---|---|
| Fallow | GitHub code scanning (SARIF) + PR comment | `.fallow.toml` |
| SonarCloud | sonarcloud.io dashboard | `.sonarcloud.properties` |
| CodeQL | GitHub code scanning | default setup, **not** `.github/codeql-config.yml` |
### Fallow
```bash
npx --yes [email protected] health --format json # complexity/CRAP findings
npx --yes [email protected] dead-code # unused files/exports
npx --yes [email protected] audit # changed-files review
```
Pin the version. A bare `npx fallow` resolves the `latest` dist-tag at run time
and has been observed to hand back three different versions across runs, which
makes the config's thresholds mean different things on different days.
**Scope (`ignorePatterns`).** 59 patterns covering vendored bundles (Emscripten
builds, Firebase SDK, Recast/Jolt wasm, pixi-filters, PeerJS, draco, the SHA
implementations), codemod output, generated declarations, and build artifacts.
These are byte-identical to their upstream copies and are never hand-edited.
`ignorePatterns` is confirmed to work — every listed path reports zero findings,
and listing the vendored bundles removed 2,399 of the 3,449 code-scanning alerts
that were open before.
**Thresholds (`[health]`).** `maxCrap = 200`, `maxCyclomatic = 25`,
`maxCognitive = 25`, `maxUnitSize = 200`. The defaults (`maxCrap 30`) reported
926 functions in non-vendored code, which is not actionable. The raised values
leave 154 — the genuine outliers. Current state:
```
files_analyzed 2601 functions_analyzed 12882 above_threshold 154
average_maintainability 87.9
```
Re-derive the values from `health --format json` if the tree changes materially;
do not copy them forward out of habit.
**Do not add `health.thresholdOverrides` for test suites.** One was tried and
removed: `describe()` blocks are not reported as oversized functions, so the
override matched nothing and emitted 1,520 `stale` rows. Fallow reports stale
suppression config, so dead overrides are visible rather than silent — keep it
that way.
**Inline suppression.** `// fallow-ignore-next-line <kind>` and
`// fallow-ignore-file <kind>`, where `<kind>` is e.g. `unused-export` or
`complexity`. A trailing `-- <reason>` is recorded by the suppression-hygiene
output. Prefer `rules` / `ignorePatterns` over sprinkling inline markers.
**CI.** `.github/workflows/fallow.yml`. The `audit` job (PRs) is informational
(`fail-on-issues: false`); the weekly `full` scan uploads SARIF and archives
`fallow.log`. Nothing here gates a merge — the SARIF upload is the gate's
visible surface. If you promote `audit` to a real gate, do it after the
thresholds above are stable, or every unrelated PR turns red.
### SonarCloud
**This project uses automatic analysis.** `sonar.autoscan.enabled` is `true` on
the project. Consequences that have already caused wasted effort:
- **`sonar.issue.ignore.multicriteria` cannot be configured from
`.sonarcloud.properties`.** It is a Project Settings → Issues → Exclusions
(UI) setting, and automatic analysis does not read it from the file. A
previous revision of `.sonarcloud.properties` carried ~200 lines of commented
`sonar.issue.ignore.multicriteria.e1..e88` entries plus
`scripts/sonarcloud-apply-suppressions.sh` to paste them. Both are deleted.
Do not re-add them: they were inert for two phases of remediation work and
the documented "10,289 → 3,029" progress was path exclusions and organic
expiry, not those rules.
- Only these keys are honoured: `sonar.sources`, `sonar.exclusions`,
`sonar.inclusions`, `sonar.tests`, `sonar.test.exclusions`,
`sonar.test.inclusions`, `sonar.sourceEncoding`, `sonar.cpd.exclusions`,
`sonar.python.version`.
- **Analysis logs are not available**, which is why the state below has to be
measured through the API.
**What is actually scanned.** Measured via the SonarCloud API:
```
files 822
ncloc 180,894
Extensions 346 files GDevelop.js 325
GDJS 86 files newIDE 44 (all under newIDE/app/scripts)
SharedLibs 21 files
```
**`newIDE/app/src` — the editor source, ~2,350 files — is not in the index.**
Every query for it returns nothing; its files are absent from the component
tree. The `files` metric dropped 3,921 → 819 on 2026-09-13 and has not moved
since. The cause could not be determined from outside (no analysis logs), and it
is **not** the `sonar.exclusions` list — `newIDE/app/src` is not excluded by it.
Probe the current state with:
```bash
curl -fs "https://sonarcloud.io/api/measures/component?component=Heretek-AI_GDevelop-Heretek&metricKeys=files,ncloc" \
| python3 -m json.tool
curl -fs "https://sonarcloud.io/api/components/tree?component=Heretek-AI_GDevelop-Heretek&qualifiers=FIL&ps=1" \
| python3 -c "import json,sys; print('files:', json.load(sys.stdin)['paging']['total'])"
```
This gap has a real cost: the fork's own AI code (`newIDE/app/src/AI/`,
`AiGeneration/Studio/`) gets no analysis at all. Do **not** "fix" it by adding
`newIDE/app/src/**` to `sonar.exclusions` — that would make the blind spot
permanent and deliberate instead of unexplained. Resolve it in the SonarCloud
UI (Administration → Analysis Method / Analysis Scope), or switch the project to
CI-based analysis, which also makes the multicriteria rules real.
**The residual issue count is mostly not this fork's code.** Of 2,845 open
issues measured, **2,526 sit in files byte-identical to upstream** and 319 in
fork-added or fork-modified files. Fixing the upstream-identical ones means
either diverging for style (which the sync then fights) or re-fixing them after
every sync. Leave them; the point of the dashboard here is to catch *new* issues
in *fork* code.
**Drift guard.** `scripts/sonarcloud-drift.js` + `.sonarcloud-drift-baseline`
(committed). It self-seeds the baseline when the file is missing, writes
`count` / `baseline` / `seeded` to `$GITHUB_OUTPUT`, and is enforced weekly by
the `sonarcloud-drift` job in `fallow.yml` — which is now a real gate (it was
`continue-on-error` against a 1,500 ceiling that predated the exclusions and was
never reachable). The ceiling is `baseline + 10%`.
```bash
node scripts/sonarcloud-drift.js # report + compare
node scripts/sonarcloud-drift.js --update-baseline # after a legitimate change
```
### CodeQL
Code scanning runs **default setup** (languages: `actions`, `c-cpp`,
`javascript`, `python`, `typescript`; `extended` query suite).
`.github/codeql-config.yml` is **not read** — default setup only honours a config
file named by the `github-codeql-config-file` repository property, which is
unset. Proof: the file excludes `**/__tests__/**`, yet an open alert exists in
`GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js`. See the header of
that file for the two ways to make it real.
Third-party actions are pinned to full commit SHAs with a trailing `# vN`
comment, and every workflow declares an explicit `permissions:` block. Keep both
properties when editing a workflow: Dependabot's `github-actions` ecosystem
updates SHA pins as long as the version comment is present, and the
`actions/unpinned-tag` / `actions/missing-workflow-permissions` alerts return
otherwise. `.github/workflows/**` is restored from local `master` by
`upstream-sync.yml`, so upstream will never overwrite these changes.
### Which alerts to fix, and which to leave
| Finding | Action |
|---|---|
| Anything in a fork-added or fork-modified file | **Fix it.** It is the fork's code. |
| Anything in a workflow file | **Fix it.** Workflows are fork-owned and sync-protected. |
| Anything in a file byte-identical to upstream | **Leave it**, or fix it upstream. Editing it creates divergence the sync then re-resolves forever. |
| Vendored bundle / generated declaration | Should not appear. If it does, the path is missing from `ignorePatterns` / `sonar.exclusions` — add it there, do not edit the file. |
---
## 3. Upstream sync
`.github/workflows/upstream-sync.yml` runs daily and is **PR-only**; `master` is
never modified automatically. It merges `upstream/master` into
`upstream-sync-<date>`, restores `.github/workflows/` from local `master`, and
verifies five BYOK files still exist.
Two things to know:
- **`.github/workflows/` is restored from local `master` on every sync.** A
change to a workflow is safe from being overwritten, and upstream workflow
fixes will never arrive. Apply them by hand if they matter.
- **The BYOK integrity check is path-based.** It guards
`src/AI/CustomAIClient.js`, `src/AI/CustomAIClient.spec.js`,
`src/AiGeneration/AiConfiguration.js`,
`src/MainFrame/Preferences/PreferencesDialog.js`, and
`src/MainFrame/Preferences/PreferencesProvider.js`. Renaming or moving any of
those silently disables the guard. `fork-divergence.json` now covers the same
ground more broadly — if you move a BYOK file, run `--update` and expect the
manifest diff to show it.
When a sync lands, run `node scripts/check-fork-divergence.js` before reviewing
the diff; it tells you which changes are the fork's own and which are new.
---
## 4. Repo-wide checks
```bash
node scripts/check-lockfiles.js # vulnerable-pin guard (all 10 manifests)
cd newIDE/app && npm run lint && npm run check-format && npm run check-types
cd GDJS && npm run check-types # known-red baseline: see AGENTS.md
```
`AGENTS.md` holds the architecture map, build/test commands, and the conventions
per language area. It is the right place for "how is this repo laid out"; this
file is the right place for "how do I keep it maintainable".
-383
View File
@@ -1,383 +0,0 @@
# SonarCloud Phase 2 Remediation Plan
## Context
Phase 1 (PR #44, merged) dropped the SonarCloud open-issue count from **10,289 → 3,649** (-64.5%) via path-based exclusions, per-rule multicriteria suppressions, and ~10 BLOCKER / MAJOR vulnerability fixes. The SonarCloud Quality Gate (which had failed on PR #5) is now **passing on every CI run**.
This Phase 2 plan attacks the remaining **3,649 open issues** with the same playbook: configuration exclusions first, targeted real fixes, then multicriteria suppressions for the volume.
**Goal:** reduce the open count to a stable ~500-1,000 (a 70-85% reduction from Phase 1's baseline), without dragging the PR review process through 1,000+ style-only diffs.
**User decision captured (2026-08-20):** Phase 2 follows the same approach as Phase 1 — config + small fixes + suppressions. No attempt to drive the count to zero.
---
## Phase 1 baseline (live state, 2026-08-20)
| Bucket | Total | Δ vs Phase 1 start |
|---|---:|---:|
| **Total open** | **3,649** | -6,640 |
| **MAJOR** | 1,175 | -2,682 |
| **CRITICAL** | 663 | -1,464 |
| **MINOR** | 1,614 | -2,098 |
| **INFO** | 171 | -357 |
| **BLOCKER** | 26 | -39 |
| CODE_SMELL | 3,540 | -6,436 |
| VULNERABILITY | 69 | -97 |
| BUG | 40 | -107 |
| Effort total | 25,619 min | -50,420 min |
---
## Remaining inventory (3,649 issues)
### Top 25 rules by count
| Count | Rule | Severity | Notes |
|---:|---|---|---|
| 275 | javascript:S101 | MINOR | Class naming convention |
| 228 | typescript:S6582 | MINOR | Single-char variable names |
| 209 | typescript:S1186 | CRITICAL | Empty methods (often interface declarations) |
| 170 | javascript:S878 | MAJOR | `==` vs `===` |
| 144 | typescript:S1135 | INFO | TODO markers |
| 133 | javascript:S7773 | MINOR | Mixed tabs/spaces |
| 131 | javascript:S3504 | CRITICAL | var → const/let |
| 123 | typescript:S3776 | CRITICAL | Cognitive complexity |
| 99 | typescript:S7741 | MINOR | Refactor |
| 90 | cpp:S3471 | MINOR | C++ |
| 82 | typescript:S2933 | MAJOR | TODO |
| 78 | javascript:S6679 | MAJOR | Refactor |
| 72 | typescript:S4138 | MINOR | `==` vs `===` |
| 72 | typescript:S1874 | MINOR | Naming |
| 70 | cpp:S5416 | MINOR | C++ |
| 65 | javascript:S7772 | MINOR | `node:` prefix (already done in some files) |
| 58 | cpp:S3230 | MAJOR | C++ |
| 56 | javascript:S6582 | MINOR | Same as ts version |
| 55 | typescript:S3358 | MAJOR | Nested ternaries |
| 54 | typescript:S6660 | MAJOR | Refactor |
| 51 | typescript:S6535 | MAJOR | Refactor |
| 46 | javascript:S3776 | CRITICAL | Cognitive complexity |
| 46 | typescript:S7773 | MINOR | Mixed tabs/spaces |
| 46 | typescript:S1444 | MINOR | Private field |
| 43 | cpp:S3490 | CRITICAL | C++ |
### By type
| Type | Count |
|---|---:|
| CODE_SMELL | 3,540 |
| VULNERABILITY | 69 |
| BUG | 40 |
### Hot files (where the volume sits)
| File | Issues | Strategy |
|---|---:|---|
| `Extensions/Effects/pixi-filters/filter-color-gradient.js` | 164 | Inspect — likely a code-mod output; add to exclusions |
| `Extensions/Physics2Behavior/physics2runtimebehavior.ts` | 145 | Real code, suppress with rationale |
| `Extensions/Physics3DBehavior/JsExtension.js` | 115 | Add to exclusions (generated extension) |
| **`SharedLibs/ThreeAddons/src/examples/jsm/loaders/GLTFLoader.js`** | 104 | **Exclude — vendored three.js examples** |
| `GDJS/Runtime/events-tools/commontools.ts` | 82 | Real code, suppress |
| `GDevelop.js/Bindings/Wrapper.cpp` | 77 | C++, suppress |
| `GDJS/Runtime/InGameEditor/InGameEditor.tsx` | 75 | Real code, suppress |
| `GDJS/Runtime/debugger-client/hot-reloader.ts` | 65 | Real code, suppress |
| `Extensions/PlatformBehavior/platformerobjectruntimebehavior.ts` | 52 | Real code, suppress |
| `GDJS/Runtime/runtimeobject.ts` | 45 | Real code, suppress |
| `GDevelop.js/__tests__/Vector.js` | 45 | Test file, small batch fix |
| `GDevelop.js/__tests__/Core.js` | (in counts above) | BLOCKER — fix test assertions |
| `GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts` | 40 | Real code, suppress |
### BLOCKER (26 total)
| Type | Count | Examples |
|---|---:|---|
| `pythonsecurity:S2083` | 1 | `fix-codemod-syntax.py:601` (new location — same issue as Phase 1, different line) |
| `javascript:S2187` | 1 | `Physics3DRuntimeBehavior.spec.js` ("add tests") |
| `javascript:S2703` | 1 | `platformerobjectruntimebehavior.benchmark.js` (missing `var`) |
| `javascript:S2699` / `S2970` | 23 | `GDevelop.js/__tests__/Core.js` etc. ("expect without assertion") |
### CRITICAL (663 total)
| Rule | Count | Type |
|---|---:|---|
| `typescript:S1186` | 209 | Empty methods (often interface declarations) |
| `javascript:S3504` | 131 | `var` → `const`/`let` |
| `typescript:S3776` | 123 | Cognitive complexity |
| `javascript:S3776` | 46 | Cognitive complexity |
| `cpp:S3490` | 43 | C++ |
| `typescript:S6861` | 20 | "Add a default case" |
| `typescript:S3504` | 19 | Same as js version |
| `python:S3776` | 10 | Python cognitive complexity |
| `cpp:S5025` | 10 | C++ |
| `typescript:S2819` | 8 | postMessage target origin (real security) |
| `javascript:S1523` | 6 | `eval`/`new Function` |
| `cpp:S3776` | 5 | C++ |
| `cpp:S1003` | 5 | C++ |
| `javascript:S2430` | 3 | Should use `===`/spread |
| `typescript:S1523` | 2 | `eval`/`new Function` |
### VULNERABILITY (69 total)
| Rule | Count | Severity |
|---|---:|---|
| `typescript:S2245` | 10 | MAJOR — `Math.random()` for crypto |
| `jssecurity:S8707` | 9 | MAJOR — LLM CLI path escape |
| `javascript:S1523` | 6 | CRITICAL — `eval`/`new Function` |
| `javascript:S2245` | 5 | MAJOR — `Math.random()` for crypto |
| `jssecurity:S7044` | 5 | MAJOR — URL path from user data |
| `jssecurity:S8476` | 5 | MINOR — Tainted data → URL |
| `shell:S6505` | 11 | MAJOR — `--ignore-scripts` missing |
| `typescript:S2819` | 8 | CRITICAL — postMessage target origin |
| `typescript:S1523` | 2 | CRITICAL |
| `jssecurity:S8705` | 1 | MAJOR — LLM sandbox escape |
| `text:S8564` | 2 | MAJOR — Lock file missing |
| `jssecurity:S5144` | 1 | MAJOR — URL from user data |
| `pythonsecurity:S2083` | 1 | BLOCKER — path from user data |
| `typescript:S5148` | 1 | MINOR — `noopener` |
| `typescript:S5332` | 1 | MINOR — `ws` vs `wss` |
| `javascript:S5443` | 1 | CRITICAL |
### BUG (40 total)
| Rule | Count | Severity |
|---|---:|---|
| `typescript:S905` | 8 | MAJOR — unused parameter |
| `typescript:S4143` | 5 | MAJOR — `await` on non-Promise |
| `javascript:S1763` | 4 | MAJOR — same expression on both sides |
| `typescript:S4822` | 3 | MAJOR — outer `await` not awaited |
| `Web:S5254` | 3 | MAJOR — HTML `lang` attribute |
| `Web:PageWithoutTitleCheck` | 2 | MAJOR — missing `<title>` |
| `javascript:S2871` | 2 | CRITICAL — re-aliased sort |
| `javascript:S3403` | 2 | MAJOR — `this` shadowing |
| `typescript:S2201` | 2 | MAJOR — return in `map` ignored |
| `typescript:S1751` | 2 | MAJOR — invalid loop |
| `javascript:S1764` | 1 | MAJOR — identical sub-expressions |
| `typescript:S1656` | 1 | MAJOR — `Object.hasOwn` vs `hasOwnProperty` |
| `typescript:S1764` | 1 | MAJOR |
| `typescript:S4335` | 1 | CRITICAL — `throw` of non-Error |
| `typescript:S7739` | 1 | MAJOR — `then` on object |
| `typescript:S7736` | 1 | MINOR — negated `==` |
| `typescript:S4158` | 1 | MINOR |
| `typescript:S905` | 1 | MAJOR |
| `javascript:S905` | 1 | MAJOR |
---
## Remediation plan
### Phase 1 — Configuration: exclusions (closes ~700-1,000)
Add to `.sonarcloud.properties`:
```
sonar.exclusions += \
SharedLibs/ThreeAddons/**,\
Extensions/Physics3DBehavior/JsExtension.js,\
Extensions/Effects/pixi-filters/filter-color-gradient.js,\
GDevelop.js/Bindings/Wrapper.cpp,\
GDevelop.js/Bindings/postjs.js
```
Reasoning per addition:
- `SharedLibs/ThreeAddons/src/examples/jsm/loaders/GLTFLoader.js` — vendored from three.js examples; we don't maintain it.
- `Extensions/Physics3DBehavior/JsExtension.js` — generated extension (matches the pattern in `Extensions/Physics2Behavior/JsExtension.js` already in Phase 1 suppressions).
- `Extensions/Effects/pixi-filters/filter-color-gradient.js` — single 152-line file that looks like a port of a pixi filter; 164 issues against it is the volume from a single file.
- `GDevelop.js/Bindings/Wrapper.cpp` / `postjs.js` — generated C++ bindings (~90+10 issues combined).
### Phase 2 — Real fixes (closes ~30-50 vulnerabilities + bugs)
In priority order:
1. **`pythonsecurity:S2083` BLOCKER** in `fix-codemod-syntax.py:601` — path comes from `SRC_DIR.rglob()`; add a top-level traversal guard in `main()` once, not just in `_process_file`.
2. **`typescript:S2819` (8 CRITICAL)** — `postMessage`/`window.addEventListener('message')` without target origin. Fix in:
- `Extensions/PlayerAuthentication/playerauthenticationtools.ts` (line 60, 1029)
- `Extensions/Multiplayer/multiplayertools.ts` (lines 691, 850, 884, 1589)
- `GDJS/Runtime/debugger-client/window-message-debugger-client.ts` (lines 27, 37)
3. **`javascript:S5443` (1 CRITICAL)** — appears in `Extensions/Events/...` (TBD). Need to find specific file.
4. **`typescript:S1523` (8 CRITICAL JS/TS combined)** — `eval` use in tests; add `// nosonar` with rationale as in Phase 1.
5. **`typescript:S4143` (5 MAJOR)** — `await` on non-Promise; fix in `multiplayertools.ts` and similar.
6. **`javascript:S2871` (2 CRITICAL)** — sort re-aliasing; fix in `Variable.ts`, etc.
7. **`typescript:S4335` (1 CRITICAL)** — `throw` of non-Error.
8. **`Web:S5254` (3 MAJOR)** — add `lang="en"` to `<html>` in `index.html` files.
9. **`Web:PageWithoutTitleCheck` (2 MAJOR)** — add `<title>`.
10. **`jssecurity:S8707` (9 MAJOR)** — additional LLM CLI path escapes, similar to Phase 1 fixes.
### Phase 3 — Test quality fixes (closes ~25-30 BLOCKERs in `GDevelop.js/__tests__/`)
The 23 BLOCKER `S2970` ("expect without assertion") and `S2699` ("add assertion to test") are concentrated in:
- `GDevelop.js/__tests__/Core.js` (look for `expect(…)` and add an actual assertion like `expect(x).toBe(...)`)
- `GDevelop.js/__tests__/GDJS.js` (lines 760, 765)
- `GDevelop.js/__tests__/SerializerBenchmark.js` (lines 24-72)
- `GDevelop.js/__tests__/Vector.js` (lines 47, 49)
Also:
- `S2187` — `Physics3DRuntimeBehavior.spec.js` (penalty spec, add a real test or delete the file).
- `S2703` — `platformerobjectruntimebehavior.benchmark.js:13` (add `var`/`let`/`const`).
These are mechanical fixes that resolve a lot of BLOCKERs.
### Phase 4 — Suppressions (closes ~2,000-2,500)
Update the multicriteria block in `.sonarcloud.properties` (server-side, operator pastes into SonarCloud UI). Add entries for the new top rules:
```
# New entries to add to the multicriteria block:
typescript:S101 (class naming convention — accepted style)
typescript:S6582 (single-char names — accepted for coordinate vars)
typescript:S1186 (empty methods — often interface declarations)
typescript:S1135 (TODO markers — accepted)
typescript:S7741 (refactor — accepted)
typescript:S2933 (TODO — accepted)
typescript:S6660 (refactor — accepted)
typescript:S6535 (refactor — accepted)
typescript:S1874 (parameter naming — accepted)
typescript:S1444 (private field — accepted)
typescript:S7741 (refactor — accepted)
typescript:S4138 (== vs === — TypeScript handles this)
typescript:S6582 (same as ts)
typescript:S6660 (refactor — accepted)
typescript:S6535 (refactor — accepted)
typescript:S3358 (nested ternaries — accepted style)
typescript:S6861 (default case — accepted)
typescript:S7741 (refactor — accepted)
Web:PageWithoutTitleCheck (after we add <title> in fix)
Web:S5254 (after we add lang attribute in fix)
text:S8564 (lockfile missing — accepted for vendored libs)
typescript:S905 (unused parameter — accepted for interface stubs)
typescript:S4822 (non-awaited async — accepted for fire-and-forget)
typescript:S1764 (identical sub-expressions — accepted)
typescript:S1656 (Object.hasOwn)
typescript:S4158 (info)
javascript:S1763 (same expression on both sides)
typescript:S2201 (return in map)
typescript:S1751 (invalid loop)
typescript:S7736 (negated ==)
typescript:S7739 (then on object)
typescript:S2871 (sort re-aliasing)
typescript:S3403 (this shadowing)
javascript:S878 (== vs === — accepted in some legacy libs)
javascript:S3504 (var → const/let — accepted in vendored)
javascript:S7773 (mixed tabs/spaces — accepted in legacy scripts)
javascript:S7772 (node: prefix — already done in some files)
javascript:S6679 (refactor — accepted)
javascript:S6582 (single-char names)
javascript:S2630 (else after return)
javascript:S2430 (== vs spread)
javascript:S2699 (assertion in test)
typescript:S3776 (cognitive complexity — accepted in vendored + tests)
javascript:S3776
cpp:S3471 (C++)
cpp:S5416 (C++)
cpp:S3230 (C++)
cpp:S3490 (C++)
cpp:S5025 (C++)
cpp:S3776 (C++)
cpp:S1003 (C++)
```
Estimate: suppressions close ~2,000 issues. Combined with current Phase 1 suppressions, total open count drops to **~500-1,000**.
### Phase 5 — CI guard updates
The `sonarcloud-drift.js` script's threshold of 4,500 needs lowering to ~1,500 after Phase 2 suppressions land. Update the threshold in `.github/workflows/fallow.yml` and `scripts/sonarcloud-drift.js`.
---
## Critical files to modify
| File | Purpose |
|---|---|
| `.sonarcloud.properties` | Add `SharedLibs/ThreeAddons/**`, generated extension exclusions, suppressions block |
| `newIDE/app/scripts/fix-codemod-syntax.py` | Add top-level path traversal guard in `main()` |
| `Extensions/PlayerAuthentication/playerauthenticationtools.ts` | `postMessage` target origin |
| `Extensions/Multiplayer/multiplayertools.ts` | `postMessage` target origin |
| `GDJS/Runtime/debugger-client/window-message-debugger-client.ts` | `postMessage` target origin |
| `GDevelop.js/__tests__/Core.js` | Add real assertions to `expect(something)` calls |
| `GDevelop.js/__tests__/GDJS.js` | Same |
| `GDevelop.js/__tests__/Vector.js` | Same |
| `GDevelop.js/__tests__/SerializerBenchmark.js` | Same |
| `GDJS/Runtime/index.html` | Add `<title>` and `lang="en"` |
| `GDJS/Runtime/FacebookInstantGames/index.html` | Same |
| `GDJS/Runtime/Cordova/www/index.html` | Same |
| `.github/workflows/fallow.yml` | Lower drift threshold |
| `scripts/sonarcloud-drift.js` | Lower threshold default |
---
## Reused existing infrastructure
| Concern | Reuse |
|---|---|
| Lockfile guard | `scripts/security/check-lockfiles.js` (Phase 1 infrastructure) |
| Fallow CI | `.github/workflows/fallow.yml` (PR + master + weekly) |
| SonarCloud REST API | `scripts/sonarcloud-drift.js` |
| Test runner | `npm test` in `newIDE/app` (Jest) |
| Linter | `npm run lint` (`eslint --max-warnings=0`) |
| Formatter | `npm run check-format` (prettier 1.15.3) |
---
## Verification (end-to-end)
After every phase:
```bash
# CI guard
node scripts/security/check-lockfiles.js # exit 0
# Lint and format
cd newIDE/app && npm run lint # clean
cd newIDE/app && npm run check-format # clean
# Tests
cd newIDE/app && npm test -- --watchAll=false --ci # 1206 passed
# Static analysis
npx fallow audit --format compact # No issues in changed files
# SonarCloud API
curl -s "https://sonarcloud.io/api/issues/search?componentKeys=Heretek-AI_GDevelop-Heretek&issueStatuses=OPEN&ps=1" | python3 -c "
import json,sys; print('Open:', json.load(sys.stdin)['total'])"
# Drift
node scripts/sonarcloud-drift.js # growth within 10% threshold
```
### Final success criteria
- **Open SonarCloud count** drops from ~3,649 to **~500-1,000** (a 70-85% reduction from current state).
- **SonarCloud Code Analysis** on the PR still passes.
- **No regression** in the newIDE/app Jest suite (1,206 tests).
- **No new CI failures** introduced.
- **PR #44's CI pass state** preserved.
---
## Risks & open questions
1. **The BLOCKER `pythonsecurity:S2083`** in `fix-codemod-syntax.py` is a recurring false-positive — the path comes from `SRC_DIR.rglob()` which is a fixed source. My previous fix used a per-file guard; the new location (line 601 vs the old 589) might be a duplicate flagging or the same path elsewhere. Need to inspect.
2. **`test:typescript:S2819`** (postMessage target origin) — 8 alerts in `PlayerAuthentication` and `Multiplayer` extensions. These are real security gaps; the fix is `event.source.postMessage(payload, targetOrigin)` style and `event.origin` validation. Approximate diff: ~30 lines.
3. **C++ rule volume** (`cpp:S3471`, `cpp:S5416`, `cpp:S3230`, `cpp:S3490`, `cpp:S5025`, `cpp:S3776`, `cpp:S1003`) — 366+ issues. The C++ code (Core/, GDevelop.js/Bindings/, Extensions/*) is generated from code-mod output and shouldn't be hand-edited. Suppression is the right answer.
4. **`jssecurity:S8707`** (9 MAJOR) — the LLM CLI path escape pattern is also in sub-commands I didn't iterate over in Phase 1. Need to enumerate and fix each.
5. **DRIFT threshold** — start at 1,500 after Phase 2; tune if needed.
---
## Critical files quick-reference
- `/home/john/Projects/GDevelop/SONARCLOUD_REMEDIATION_PLAN.md` — Phase 1 plan (executed)
- `/home/john/Projects/GDevelop/SONARCLOUD_PHASE2_PLAN.md` — **this file** (Phase 2)
- `/home/john/Projects/GDevelop/.sonarcloud.properties` — exclusions + suppressions
- `/home/john/Projects/GDevelop/.sonar-issues-cache/phase-2-p*.json` — raw cache (3,649 issues, ignored by git)
- `/home/john/Projects/GDevelop/scripts/sonarcloud-drift.js` — drift detection
- `/home/john/Projects/GDevelop/.github/workflows/fallow.yml` — CI guard
- PR #44 — merged PR that established Phase 1
-305
View File
@@ -1,305 +0,0 @@
# SonarCloud Phase 3 Remediation Plan
## Context
Phases 1 (PR #44) and 2 (PR #50) dropped the SonarCloud open-issue count from **10,289 → 3,649 → 3,029** (-70.6%) via path exclusions, real fixes (security vulnerabilities + test-quality bugs), and per-rule suppressions documented in the operator's SonarCloud UI.
This Phase 3 plan attacks the remaining **3,029 open issues** with the same playbook, but adjusted for what didn't get fully applied in Phase 2. The Phase 2 multicriteria block is **server-side** — many of its rules (S101, S6582, S1186, S878, S1135, S7773, S3504, S3776, S7741, cpp:*) are STILL firing because the operator hasn't pasted the block into SonarCloud → Project Settings → Issues → Exclusions yet.
**Goal:** drop the open count to ~500–1,000 (a 70-85% reduction from current) by:
1. Fixing the remaining real BLOCKER + CRITICAL bugs (security + test quality)
2. Adding path exclusions for newly-merged hot files that emerged after Phase 2
3. Adding **Phase 2's still-unapplied suppressions** to `.sonarcloud.properties` and extending with Phase 3 rules
4. Adding an explicit operator-runnable script that pastes everything at once
**User decision captured (2026-08-20):** same approach as Phase 2. Don't try to drive the count to zero.
---
## Phase 3 inventory (live, fetched 2026-08-20)
| Bucket | Count | Δ vs Phase 2 start (3,649) |
|---|---:|---:|
| **Total open** | **3,029** | -620 |
| BLOCKER | 9 | -17 |
| CRITICAL | 600 | -63 |
| MAJOR | 931 | -244 |
| MINOR | 1,328 | -286 |
| INFO | 161 | -10 |
| CODE_SMELL | 2,931 | -609 |
| VULNERABILITY | 68 | -1 |
| BUG | 30 | -10 |
| Effort total | 19,752 min | -5,867 min |
The 620-issue drop happened organically:
- 17 BLOCKER `S2970` test-quality fixes in `GDevelop.js/__tests__/Core.js` (PR #50)
- ~600 issues auto-close as aged or accepted by SonarCloud's analysis lifecycle
---
## Remaining BLOCKER (9 total)
The 9 remaining BLOCKER are all in tests:
| Count | Type | Rule | File:Line | Action |
|---:|---|---|---|---|
| 1 | VULN | `pythonsecurity:S2083` | `newIDE/app/scripts/fix-codemod-syntax.py:605` | **Fix** — same as Phase 1/2; line moved |
| 1 | SMELL | `javascript:S2187` | `Extensions/Physics3DBehavior/tests/Physics3DRuntimeBehavior.spec.js` | Add a minimal real test, or delete the empty test file |
| 1 | SMELL | `javascript:S2703` | `Extensions/PlatformBehavior/benchmarks/platformerobjectruntimebehavior.benchmark.js:13` | Add `var`/`let`/`const` keyword |
| 4 | SMELL | `javascript:S2699` | `GDevelop.js/__tests__/SerializerBenchmark.js:24,39,55,72` | Add at least one assertion |
| 2 | SMELL | `javascript:S2699` | `GDevelop.js/__tests__/GDJS.js:760,765` | Add at least one assertion |
These are all mechanical and small. 8 of 9 are test fixes that prevent a real BUG (incomplete tests).
## Remaining CRITICAL real issues (VULNERABILITY + BUG, not just code smells)
| Count | Type | Rule | Example | Action |
|---:|---|---|---|---|
| 8 | VULN | `typescript:S2819` | postMessage wildcard target origin (5 files) | **Already fixed in Phase 2** but the `// nosonar` placement was wrong. Phase 3 commit 9ba765c moves the comments to the right line. Re-analyze to confirm. |
| 1 | VULN | `typescript:S1523` | `GDJS/Runtime/gameplay-tests/gameplay-test-runner.ts:2951` | Add `// nosonar` with rationale (test runner code, sandboxed) |
| 6 | VULN | `javascript:S1523` | `GDevelop.js/TestUtils/CodeGenerationHelpers.js:82,93,108,161,227,463` | Same — test utility code |
| 1 | VULN | `javascript:S5443` | `GDevelop.js/TestUtils/FakeAbstractFileSystem.js:15` | Investigate: writable-dir safety in test fixture |
| 1 | VULN | `typescript:S4335` | `GDJS/Runtime/CustomRuntimeObject.ts:22` | Add `// nosonar: typescript:S4335` with rationale (cross-type assignment intentional for runtimeobject) |
| 1 | VULN | `javascript:S2871` | `newIDE/app/scripts/lib/ArrayHelpers.js:32` | Provide locale-aware comparator (real bug if used in i18n contexts) |
| 10 | VULN | `python:S3776` | Various Python files (extract to a helper) | Refactor or accept (mostly tooling scripts) |
| 10 | VULN | `typescript:S2245` | `Math.random()` use | Document or override |
| 9 | VULN | `jssecurity:S8707` | LLM CLI path escape (script files) | Validate CLI args, similar to Phase 1/2 |
## Top 20 hot files (where the volume sits)
| File | Issues | Strategy |
|---|---:|---|
| `Extensions/Physics2Behavior/physics2runtimebehavior.ts` | 145 | Real code, suppress (Phase 2-style) |
| `GDJS/Runtime/events-tools/commontools.ts` | 82 | Real code, suppress |
| `GDJS/Runtime/InGameEditor/InGameEditor.tsx` | 75 | Real code, suppress |
| `GDJS/Runtime/debugger-client/hot-reloader.ts` | 65 | Real code, suppress |
| `Extensions/PlatformBehavior/platformerobjectruntimebehavior.ts` | 52 | Real code, suppress |
| `GDJS/Runtime/gameplay-tests/gameplay-test-runner.ts` | 48 | Test runner; also has S1523 |
| `GDJS/Runtime/runtimeobject.ts` | 45 | Real code, suppress |
| `GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts` | 40 | Real code, suppress |
| `Extensions/TileMap/JsExtension.js` | 37 | Generated extension shell |
| `Extensions/Physics2Behavior/JsExtension.js` | 33 | Generated extension shell |
| `newIDE/app/scripts/extract-changelog.js` | 33 | Build script |
| `Extensions/Firebase/B_firebasetools/D_cloudfirestoretools.ts` | 32 | Bundled Firebase JS extension |
| `Extensions/Multiplayer/multiplayertools.ts` | 31 | Real code, suppress (had been 42 in Phase 2; the 8 S2819 fixes dropped it) |
| `GDJS/Runtime/variable.ts` | 31 | Real code, suppress |
| `Extensions/TweenBehavior/TweenManager.ts` | 31 | Real code, suppress |
| `Extensions/NavMeshPathfinding/JsExtension.js` | 28 | Generated extension shell |
| `GDJS/Runtime/gd.ts` | 25 | Core runtime file |
| `Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts` | 24 | Real code |
| `Extensions/Effects/pixi-filters/filter-glitch.js` | 24 | Vendored pixi filter port |
| `Extensions/Firebase/B_firebasetools/D_authtools.ts` | 24 | Bundled Firebase |
## Top 50 rules (Phase 3 inventory)
| Count | Rule | Severity | Notes |
|---:|---|---|---|
| 275 | `javascript:S101` | MINOR | Class naming convention |
| 228 | `typescript:S6582` | MINOR | Single-char names |
| 209 | `typescript:S1186` | CRITICAL | Empty methods (interface stubs) |
| 144 | `typescript:S1135` | INFO | TODO markers |
| 135 | `javascript:S878` | MAJOR | `==` vs `===` |
| 123 | `typescript:S3776` | CRITICAL | Cognitive complexity |
| 110 | `javascript:S3504` | CRITICAL | `var` → `const`/`let` |
| 99 | `typescript:S7741` | MINOR | Refactor |
| 88 | `cpp:S3471` | MINOR | C++ |
| 82 | `typescript:S2933` | MAJOR | TODO |
| 72 | `typescript:S4138` | MINOR | `==` vs `===` |
| 72 | `typescript:S1874` | MINOR | Parameter naming |
| 65 | `javascript:S7772` | MINOR | `node:` prefix |
| 58 | `javascript:S7773` | MINOR | Mixed tabs/spaces |
| 58 | `cpp:S3230` | MAJOR | C++ |
| 55 | `typescript:S3358` | MAJOR | Nested ternaries |
| 54 | `typescript:S6660` | MAJOR | Refactor |
| 51 | `typescript:S6535` | MAJOR | Refactor |
| 49 | `typescript:S107` | MAJOR | **NEW** — function has too many parameters |
| 46 | `typescript:S7773` | MINOR | Mixed tabs/spaces |
| 46 | `typescript:S1444` | MINOR | Private field |
| 41 | `typescript:S107` | (same as above) | |
| 38 | `cpp:S3490` | CRITICAL | C++ |
| 34 | `typescript:S1121` | MAJOR | Sub-expression assignment |
| 26 | `typescript:S1121` | (same) | |
| 24 | `typescript:S7762` | MAJOR | `.remove()` vs `.removeChild()` |
| 23 | `typescript:S6035` | MAJOR | Replace alternation with character class |
| 23 | `typescript:S7740` | (combined) | |
| 20 | `typescript:S6671` | MAJOR | Promise rejection should be Error |
| 20 | `javascript:S6557` | MAJOR | `String#startsWith` |
| 20 | `typescript:S6861` | CRITICAL | Add default case in switch |
**Key observation:** Many of these still appear because the **Phase 2 suppressions haven't been applied server-side**. Phase 3 makes them more discoverable by:
1. Generating a single, runnable script the operator can paste
2. Adding Phase 2's still-unapplied rules to Phase 3's multicriteria block
3. Documenting which block to apply when
---
## Remediation plan
### Phase 3.1 — Configuration exclusions (closes ~300-500)
Add 4-5 new path exclusions to `.sonarcloud.properties`:
- `Extensions/Effects/pixi-filters/filter-glitch.js` (24, similar to filter-color-gradient)
- `Extensions/Firebase/B_firebasetools/**` (~50, bundled Firebase JS)
- `Extensions/TileMap/JsExtension.js` (37, generated extension shell)
- `Extensions/NavMeshPathfinding/JsExtension.js` (28, generated extension shell)
And to `.fallow.toml` (mirror).
**Estimated impact:** drops ~150 issues.
### Phase 3.2 — Real fixes (closes ~30-50)
In priority order:
1. **9 BLOCKER** (above) — all mechanical, ~5-line changes total
2. **`javascript:S2871`** in `newIDE/app/scripts/lib/ArrayHelpers.js:32` — provide locale-aware `String.localeCompare` comparator
3. **`typescript:S4335`** in `CustomRuntimeObject.ts:22` — add `// nosonar`
4. **`typescript:S1523`** in 8 test-runner/test-utility sites — add `// nosonar` with rationale
5. **`javascript:S5443`** in `FakeAbstractFileSystem.js:15` — investigate and either fix or add nosonar
6. **`jssecurity:S8707`** in the remaining 9 script files — add CLI-arg validation
**Estimated impact:** drops ~30 CRITICAL/MAJOR issues + 9 BLOCKERs.
### Phase 3.3 — Suppressions (closes ~2,000-2,500)
The Phase 2 multicriteria block **already contains** the e27-e63 entries — but they haven't been applied yet. The biggest immediate win for Phase 3 is:
1. **Operator action document:** Generate a single script in `scripts/sonarcloud-apply-suppressions.sh` that walks the operator through pasting the e1-e63 block into SonarCloud UI. Add a checklist in `SONARCLOUD_PHASE2_PLAN.md` that says "do this once".
2. **Add Phase 3 rules** to the multicriteria block (continuation e64+):
- `typescript:S107` (49) — function has too many parameters (over-disambiguation of methods)
- `typescript:S1121` (34) — assignment in expression
- `typescript:S7762` (24) — `.remove()` vs `.removeChild()`
- `typescript:S6035` (23) — replace alternation with character class
- `typescript:S6671` (20) — promise rejection should be Error
- `javascript:S6557` (20) — `String#startsWith`
- `typescript:S6666` (17) — spread vs `apply`
- `typescript:S7765` (17) — `.includes()` vs `.some()`
- `typescript:S7778` (16) — don't call `push()` multiple times
- `javascript:S7780` (15) — `String.raw`
- `typescript:S7750` (15) — `.find()` over `.filter()[0]`
- `typescript:S6551` (13) — default stringification
- `typescript:S7740` (11) — don't `this = harness`
- `python:S3776` (10) — Python cognitive complexity
- `typescript:S6644` (10) — refactor
- `javascript:S4144` (10) — tagged template literal
- `typescript:S2486` (9) — console.log usage
- `javascript:S8786` (9) — `'Math.random()' is insecure`
- `typescript:S3626` (9) — use rest parameters
- `typescript:S1533` (9) — refactor (closing brace placement)
- `typescript:S6571` (8) — bad magic number
- `javascript:S107` (8) — JS counterpart of typescript:S107
- `javascript:S2681` (8) — refactor (switch without default)
- `cpp:S1659` (7) — C++ refactor
- `Web:S7926` (3) — production code shouldn't have `console.log`
These all follow the same pattern: blanket suppression across the codebase with rationale. Estimated impact: drops ~1,500-2,000 issues.
### Phase 3.4 — CI guard updates
Keep drift threshold at 1,500 (already lowered in Phase 2). After Phase 3 suppressions land, the new baseline will be ~500-1,000, so consider lowering to 1,000.
### Phase 3.5 — Operator action script
Add `scripts/sonarcloud-apply-suppressions.sh` (no-op when run, but gives the operator a single command to copy-paste):
```bash
#!/usr/bin/env bash
# SonarCloud suppressions setup
# Phase 1: Project Settings → Analysis Scope → Set "sonar.exclusions"
# (paste the contents of .sonarcloud.properties's sonar.exclusions line)
# Phase 2: Project Settings → Issues → Exclusions → Multi-criteria → Add
# paste each e1..e63 entry from .sonarcloud.properties
# Phase 3: same → paste e64..e88 entries
#
# Verify with: scripts/sonarcloud-drift.js
```
This makes the operator's job a single read-and-paste instead of searching through 200+ lines of comments.
---
## Critical files to modify
| File | Purpose |
|---|---|
| `.sonarcloud.properties` | New path exclusions + extended multicriteria block (e64+) |
| `.fallow.toml` | Mirror path exclusions |
| `newIDE/app/scripts/fix-codemod-syntax.py` | S2083 fix at line 605 |
| `newIDE/app/scripts/lib/ArrayHelpers.js` | `String.localeCompare` comparator |
| `GDJS/Runtime/CustomRuntimeObject.ts` | `// nosonar: typescript:S4335` |
| `GDJS/Runtime/gameplay-tests/gameplay-test-runner.ts` | `// nosonar: typescript:S1523` × 2 |
| `GDevelop.js/TestUtils/CodeGenerationHelpers.js` | `// nosonar: javascript:S1523` × 6 |
| `GDevelop.js/TestUtils/FakeAbstractFileSystem.js` | Investigate S5443 |
| `GDevelop.js/__tests__/SerializerBenchmark.js` | Add assertions (S2699) × 4 |
| `GDevelop.js/__tests__/GDJS.js` | Add assertions (S2699) × 2 |
| `Extensions/Physics3DBehavior/tests/Physics3DRuntimeBehavior.spec.js` | Add real test (S2187) or delete |
| `Extensions/PlatformBehavior/benchmarks/platformerobjectruntimebehavior.benchmark.js` | Add `var` keyword (S2703) |
| `scripts/sonarcloud-apply-suppressions.sh` | **NEW** — operator action script |
---
## Reused existing infrastructure
| Concern | Reuse |
|---|---|
| Phase 2 suppressions block | already in `.sonarcloud.properties` (just needs operator to apply) |
| Lockfile guard | `scripts/security/check-lockfiles.js` |
| Fallow CI | `.github/workflows/fallow.yml` |
| SonarCloud REST API | `scripts/sonarcloud-drift.js` |
---
## Verification
After each sub-phase:
```bash
# CI guard
node scripts/security/check-lockfiles.js # exit 0
# Lint and format
cd newIDE/app && npm run lint # clean
cd newIDE/app && npm run check-format # clean
# Tests
cd newIDE/app && npm test -- --watchAll=false --ci # 1206 passed
cd GDevelop.js && timeout 90 npx jest --watchAll=false # ~32 passed
# Static analysis
npx fallow audit --format compact # clean
# SonarCloud API
curl -s "https://sonarcloud.io/api/issues/search?componentKeys=Heretek-AI_GDevelop-Heretek&issueStatuses=OPEN&ps=1" \
| python3 -c "import json,sys; print('Open:', json.load(sys.stdin)['total'])"
```
### Final success criteria
- Open count drops to **≤ 1,000** by close of Phase 3
- SonarCloud Quality Gate on PR stays **PASS**
- All CI checks pass (12 of 12 active)
- Phase 2 + Phase 3 suppressions applied server-side via the operator action script
- Documentation updated; future contributors can use `scripts/sonarcloud-drift.js` as the steward
---
## Risks & open questions
1. **Operator hasn't applied Phase 2 yet.** Until the operator pastes the e27-e63 multicriteria block into SonarCloud, the open count won't drop dramatically regardless of what this PR adds. Phase 3.5 is the script that makes the operator action crisp.
2. **`typescript:S107`** (too many parameters): triggering on `gd.Vector*` and `gd.Object*` methods that legitimately need 8-10 args. The fix is to break up the functions, but that's a 100+ file refactor.
3. **`python:S3776`** in `fix-flow-errors.sh` and other tooling scripts: not user-facing, low priority.
4. **C++ volume (300+)** is mostly auto-generated binding code. Phase 2's `cpp:*` suppressions cover most of it; if the operator applies them, the remaining cpp issues should drop to a manageable count.
---
## Critical files quick-reference
- `SONARCLOUD_REMEDIATION_PLAN.md` — Phase 1 plan (executed)
- `SONARCLOUD_PHASE2_PLAN.md` — Phase 2 plan (executed, suppressions waiting for operator)
- `SONARCLOUD_PHASE3_PLAN.md` — **this file** (Phase 3)
- `.sonarcloud.properties` — path exclusions + e1-e88 (server-side pending)
- `.fallow.toml` — mirror exclusions
- `.sonar-issues-cache/phase-3-p*.json` — raw cache (3,029 issues, ignored by git)
- `scripts/sonarcloud-drift.js` — drift detection
- PR #50 — merged Phase 2 PR
-263
View File
@@ -1,263 +0,0 @@
# SonarCloud Remediation Plan
## Context
SonarCloud reports **10,289 open issues** on `Heretek-AI/GDevelop-Heretek` (capped at 10,000 per API call). The previous PR (#5) closed 0 of these — it was scoped to Dependabot + secret-scanning + CodeQL, not SonarCloud. This plan picks up where that left off, scoped to what's actually fixable vs. suppressible.
**Goal:** reduce the open-issue count to a steady-state we can keep under control, without dragging the PR review process through 2,000 style-only diffs.
**User decision captured (2026-08-20):** scope the remediation around the issue profile below; suppress / ignore the rest. Do NOT try to drive the count to zero.
---
## Inventory (sampled from API, 2026-08-20)
| Bucket | Count |
|---|---:|
| **Total open** | **10,289** (API reports 10,000 — likely rounded) |
| Effort total | ~76,000 min (~1,267 hours) |
### By severity
| Severity | Count |
|---|---:|
| BLOCKER | 65 |
| CRITICAL | 2,127 |
| MAJOR | 3,857 |
| MINOR | 3,712 |
| INFO | 528 |
### By type
| Type | Count |
|---|---:|
| CODE_SMELL | 9,976 |
| VULNERABILITY | 166 |
| BUG | 147 |
### By "clean code" category
| Category | Count |
|---|---:|
| INTENTIONAL | 6,298 |
| CONSISTENT | 2,818 |
| ADAPTABLE | 1,067 |
| RESPONSIBLE | 106 |
### Hot files
| File | Issues | Notes |
|---|---:|---|
| `Extensions/Spine/spine-pixi-v7/B_spine-pixi-v7.js` | **~636** | **Vendored bundled code** (BYOK/transpile artifact). **Exclude from analysis** — single largest noise source. |
| `newIDE/app/src/EditorFunctions/index.js` | 93 | Auto-generated by code-mod from upstream `4ian/GDevelop`. **Exclude** — we don't maintain it. |
| `GDJS/Runtime/gameplay-tests/gameplay-test-runner.ts` | 48 | Test infrastructure; needs review but lower priority. |
| `newIDE/app/src/AiGeneration/AiRequestChat/ChatMessages.js` | 31 | Active code, AI/chat UI. |
| `Extensions/NavMeshPathfinding/JsExtension.js` | 28 | Generated navmesh extension. |
| `newIDE/app/src/MainFrame/index.js` | 26 | Active code. |
| `newIDE/app/scripts/fix-flow-errors.sh` | 20 | Utility script. |
### Top rules (top 20 across the 2,000 sampled issues)
| Count | Rule | Summary |
|---:|---|---|
| 240 | `javascript:S3504` | Use `const` / `let` instead of `var` |
| 225 | `javascript:S3776` | Cognitive complexity of functions should be limited |
| 224 | `javascript:S3358` | Nested ternaries should not be used |
| 84 | `javascript:S6441` | Ensure resource availability (try-with-resources / finally) |
| 67 | `javascript:S7773` | Files should not contain both tab and space-indented blocks |
| 60 | `javascript:S4138` | Avoid `assert.ok`/`assert.equal` in unit tests; use specific assertions |
| 58 | `javascript:S1121` | Assignments should not be made from within sub-expressions |
| 52 | `javascript:S6836` | Use type-specific assertions in unit tests |
| 40 | `javascript:S7778` | (style) |
| 38 | `typescript:S3776` | (same as JS) |
| 35 | `javascript:S6747` | Synchronized classes should not be used in unit tests |
| 33 | `typescript:S6582` | (style) |
| 33 | `javascript:S7769` | (style) |
---
## What's worth fixing vs. suppressing
### Tier 1 — VULNERABILITY (166 issues)
These are the only ones that move the security rating. **Subset of 2,000-sample (67 found, ~166 total):**
| Rule | Subset | Action |
|---|---:|---|
| `jssecurity:S8707` | 12 | **Fix** — LLM CLI argument escape; relevant to our BYOK work |
| `shell:S6505` + `githubactions:S6505` | 11+3 | **Fix** — `--ignore-scripts` hygiene in our CI workflows |
| `javascript:S2245` | 9 | **Fix** — `Math.random()` for crypto/security decisions |
| `javascript:S4036` | 5 | **Fix** — PATH injection in shell scripts |
| `jssecurity:S8476` | 5 | **Fix** — tainted data → client-side URL |
| `jssecurity:S7044` | 5 | **Fix** — URL path from user-controlled data |
| `javascript:S1523` (js + ts) | 5 | **Fix** — `eval`/`new Function` injection |
| `githubactions:S8543` | 3 | **Fix** — unpinned `uses:` versions in workflows |
| `jssecurity:S6350` | 2 | **Fix** — user-controlled command arg |
| `jssecurity:S2076` (BLOCKER) | 1 | **Fix** — `electron-app/app/main.js:846` |
| `jssecurity:S2083` (BLOCKER) | 1 | **Fix** — `scripts/fix-codemod-syntax.py:589` |
| `jssecurity:S8701`/`S8705` | 2 | **Fix** — LLM sandbox escapes |
| `jssecurity:S5144` | 1 | **Fix** — URL from user data |
| `jssecurity:S2076` | 1 | **Fix** — OS command from user data |
| `pythonsecurity:S2083` | 1 | **Fix** — Python path from user data |
| `githubactions:S8233` / `S7637` | 2 | **Fix** — GitHub Actions best practices |
**Estimated scope:** ~15-30 focused security patches across `electron-app/app/main.js`, build scripts, and CI workflows.
### Tier 2 — BUG (147 issues)
Real functional defects. Sample breakdown:
- `javascript:S1082` × 7: missing keyboard handler on click (accessibility, not a runtime bug)
- `javascript:S3923`, `S1764`, `S6756`, `S7739`, `S1751`, `S7736`, `S2137`, `cpp:S1048`: small handful each, scattered
**Estimated scope:** triage into 5–15 actionable fixes, suppress the rest as "won't fix" if they're false positives (e.g., S1082 is a code-smell not a bug, but SonarCloud tags it as BUG).
### Tier 3 — CODE_SMELL (9,976 issues)
Volume is dominated by:
- **Vendored bundles** (~636 from `B_spine-pixi-v7.js` alone)
- **Generated code** (~120 from `EditorFunctions/index.js`, codemod output)
- **Style nits** (var→const/let, nested ternaries, missing semicolons)
**Strategy:** suppress what we can't realistically fix; fix a small, high-value subset.
---
## Remediation plan
### Phase 1 — Configuration (no code logic)
**1a.** Exclude vendored / generated paths from SonarCloud's source set so they stop counting against us.
- Add `sonar.exclusions` (or extend `.sonarcloud.properties` if it exists) to skip:
```
Extensions/Spine/spine-pixi-v7/**
newIDE/app/src/EditorFunctions/index.js # auto-generated
newIDE/app/src/EditorFunctions/SimplifiedProject/** # codemod output
GDJS/Runtime/gameplay-tests/** # if low-value
```
- Estimated impact: **~800-1,000 issues** drop from "open" without code changes.
**1b.** Enable `sonar.issue.ignore.block` / `won't fix` for patterns we accept (intentional):
- `javascript:S3504` (var→const) — accept as `won't fix` for vendor code; fix in our active source via autofixer.
- `javascript:S3358` (nested ternaries) — accept for legacy code; refactor in active code.
- `javascript:S7773` (mixed tabs/spaces) — fix in tracked files via `prettier`.
**1c.** Drop `sonar-scanner` coverage of vendored runtime files (`pixi.js`, `three.js`, `monaco-editor`, `nanomarkdown.js`, `rbush.js`, `howler.min.js`).
### Phase 2 — Real fixes (small batch, high signal)
In priority order:
1. **2 BLOCKER** vulnerabilities (`S2076` in `electron-app/app/main.js`, `S2083` in `fix-codemod-syntax.py`).
2. **5 CRITICAL** vulnerabilities (`S8707`, `S1523` for eval, `S2245` crypto RNG, etc.).
3. **CI workflow hygiene**: `--ignore-scripts` (S6505), pinned `uses:` (S8543), minimal write permissions (S8233).
4. **PATH injection** (S4036) in build scripts.
5. **Test assertion quality** (S4138 / S6836) — replace `assert.ok(...)` with specific matchers in test files we actually maintain.
**Estimated impact:** closes ~30-50 issues of the highest severity and pulls the security rating up to A.
### Phase 3 — Suppress the rest in bulk
After Phase 1 + 2, the remaining ~9,500 issues are overwhelmingly:
- Code-style nits in long-lived files (`var` vs `const` etc.)
- Cognitive-complexity warnings in AI/Editor/Generator code (S3776)
- Nested ternaries in legacy code
- Missing `assert` argument in test cases
- Identical sub-expressions on operator sides
For these, add a `.sonarcloud.properties` ignore block (or commit per-issue `won't fix` resolutions via the UI):
```
sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6,e7
sonar.issue.ignore.multicriteria.e1.ruleKey=javascript:S3504
sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.spec.js
sonar.issue.ignore.multicriteria.e2.ruleKey=javascript:S3776
sonar.issue.ignore.multicriteria.e2.resourceKey=newIDE/app/src/EditorFunctions/**
sonar.issue.ignore.multicriteria.e3.ruleKey=javascript:S3776
sonar.issue.ignore.multicriteria.e3.resourceKey=newIDE/app/src/AiGeneration/**
sonar.issue.ignore.multicriteria.e4.ruleKey=javascript:S3358
sonar.issue.ignore.multicriteria.e4.resourceKey=Extensions/Spine/**
sonar.issue.ignore.multicriteria.e5.ruleKey=javascript:S4138
sonar.issue.ignore.multicriteria.e5.resourceKey=newIDE/app/src/EditorFunctions/**
sonar.issue.ignore.multicriteria.e6.ruleKey=javascript:S6836
sonar.issue.ignore.multicriteria.e6.resourceKey=**/*.spec.js
sonar.issue.ignore.multicriteria.e7.ruleKey=javascript:S7773
sonar.issue.ignore.multicriteria.e7.resourceKey=Extensions/Spine/**
```
**Estimated impact:** drops another ~3,000-5,000 issues by suppressing patterns we accept as intentional. Brings the open count into the **~2,000-4,000 range** (manageable steady state).
### Phase 4 — Establish a guard so it doesn't grow back
- Add a weekly CI job that diffs the SonarCloud issue count vs the previous week and fails if it grows by more than N%.
- Document the suppressions list in `SONARCLOUD_REMEDIATION_PLAN.md` (this file).
---
## Critical files to modify
- `.sonarcloud.properties` (or `sonar-project.properties`) — exclusion + multicriteria suppression config
- `.github/workflows/ci.yml` — add the "SonarCloud drift check" job
- `newIDE/electron-app/app/main.js` — fix `S2076` BLOCKER (line 846)
- `newIDE/app/scripts/fix-codemod-syntax.py` — fix `S2083` BLOCKER (line 589)
- `newIDE/app/scripts/` and `GDJS/scripts/` — fix PATH-injection + ignore-scripts
- ~5-15 files under `newIDE/app/src/` for individual S2245 / S1523 / S8476 / S7044 fixes
---
## Reused existing infrastructure
| Concern | Reuse |
|---|---|
| Existing CI workflow | extend `.github/workflows/ci.yml` with a "sonar-drift" job that calls the SonarCloud REST API (no token needed for the public project) |
| Prettier for style | `npm run check-format` (already in CI) |
| ESLint for security | already in CI |
| CodeQL workflow | already in CI; overlaps with SonarCloud JS rules |
---
## Verification (end-to-end)
After each phase:
```bash
# 1. Count remaining open issues
curl -s "https://sonarcloud.io/api/issues/search?componentKeys=Heretek-AI_GDevelop-Heretek&issueStatuses=OPEN&ps=1" \
| python3 -c "import json,sys; print('open:', json.load(sys.stdin)['total'])"
# 2. Count VULNERABILITY + BLOCKER specifically
curl -s "https://sonarcloud.io/api/issues/search?componentKeys=Heretek-AI_GDevelop-Heretek&issueStatuses=OPEN&types=VULNERABILITY&ps=1" \
| python3 -c "import json,sys; print('vuln:', json.load(sys.stdin)['total'])"
# 3. Confirm security rating went up
curl -s "https://sonarcloud.io/api/qualitygates/project_status?projectKey=Heretek-AI_GDevelop-Heretek" \
| python3 -c "import json,sys; print(json.load(sys.stdin))"
# 4. CI still green on master
cd /home/john/Projects/GDevelop && node scripts/security/check-lockfiles.js # exit 0
cd newIDE/app && npm test -- --watchAll=false --ci # 1206 passed
```
### Final success criteria
- **Open SonarCloud issue count** dropped from 10,289 to **≤ 4,000** (a ~60% reduction). Most of that drop is from configuration (exclusions + multicriteria suppressions), not code changes.
- **Security Rating on New Code** returns to **A** in the next PR's quality gate.
- **No regression** in the newIDE/app Jest suite (1,206 tests pass) or the lockfile guard.
- **Suppression list** documented in this file, so future contributors know why specific rules are excluded.
---
## Risks & open questions
1. **SonarCloud API token for write operations.** Phase 1c and Phase 3 require pushing a `sonar-project.properties` (or `.sonarcloud.properties`) to the repo, AND setting `sonar.issue.ignore.multicriteria` via the Web API. The multicriteria block is committed to the repo, but SonarCloud's "ignore" rules live server-side and need either: (a) a project-level SONAR_TOKEN repo secret, or (b) manual UI dismissal. We don't have the token; for Phase 3 I'll generate the multicriteria block in `SONARCLOUD_REMEDIATION_PLAN.md` and the operator can paste it into the SonarCloud UI under Project Settings → Issues → Exclusions.
2. **Excluding `B_spine-pixi-v7.js`** is the right move (it's vendored code we don't maintain), but the user should know that this is a "suppress by exclusion" approach, not a fix. If upstream `4ian/GDevelop` ever updates the vendored copy, the exclusions still apply — that's fine.
3. **`EditorFunctions/index.js`** is auto-generated by the codemod script in `newIDE/app/scripts/`. Excluding it prevents new issues from being filed every time the codemod runs. Same approach as Spine.
4. **The "won't fix" suppressions** may bother some contributors. Document each in this file so reviewers can challenge them. If the user wants a stricter posture (e.g., "fix all CRITICAL"), the plan expands significantly — Phase 2 alone becomes a multi-week effort.
---
## Critical files quick-reference
- `/home/john/.claude/plans/using-the-github-mcp-enumerated-meerkat.md` — Phase 1-5 security plan (closed)
- `/home/john/Projects/GDevelop/SONARCLOUD_REMEDIATION_PLAN.md` — **this file** (current)
- `/home/john/Projects/GDevelop/.sonar-issues-cache/sonar-issues-{1,2,3,4}.json` — raw API cache (2,000 issues, ignored by git)
- `/home/john/Projects/GDevelop/sonar-project.properties` or `.sonarcloud.properties` — to be added
+180
View File
@@ -0,0 +1,180 @@
{
"$comment": "Checked-in allowlist of every path this fork intentionally diverges from 4ian/GDevelop on. Regenerate with `node scripts/check-fork-divergence.js --update`. Enforced by `node scripts/check-fork-divergence.js` in CI. See MAINTENANCE.md.",
"updatedAt": "2026-09-22",
"upstreamSource": "api:4ian/GDevelop@master",
"modified": [
".github/workflows/build-storybook.yml",
".github/workflows/extract-translations.yml",
".github/workflows/gdcore-tools-hook.yml",
".github/workflows/issues.yml",
".github/workflows/update-extension-translations.yml",
".github/workflows/update-translations.yml",
".gitignore",
"Extensions/3D/A_RuntimeObject3D.ts",
"Extensions/3D/A_RuntimeObject3DRenderer.ts",
"Extensions/3D/CustomRuntimeObject3D.ts",
"Extensions/3D/CustomRuntimeObject3DRenderer.ts",
"Extensions/3D/JsExtension.js",
"Extensions/3D/Model3DObjectConfiguration.cpp",
"Extensions/3D/Model3DObjectConfiguration.h",
"Extensions/3D/Model3DRuntimeObject.ts",
"Extensions/3D/Model3DRuntimeObject3DRenderer.ts",
"Extensions/Firebase/tests/FirebaseExtension.spec.js",
"Extensions/Multiplayer/multiplayertools.ts",
"Extensions/PlatformBehavior/benchmarks/platformerobjectruntimebehavior.benchmark.js",
"Extensions/PlayerAuthentication/playerauthenticationtools.ts",
"Extensions/TileMap/tilemapruntimeobject-pixi-renderer.ts",
"GDJS/Runtime/Cordova/www/index.html",
"GDJS/Runtime/CustomRuntimeObject.ts",
"GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts",
"GDJS/Runtime/Electron/package.json",
"GDJS/Runtime/FacebookInstantGames/index.html",
"GDJS/Runtime/RuntimeInstanceContainer.ts",
"GDJS/Runtime/debugger-client/window-message-debugger-client.ts",
"GDJS/Runtime/gameplay-tests/gameplay-test-runner.ts",
"GDJS/Runtime/index.html",
"GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts",
"GDJS/Runtime/runtimeobject.ts",
"GDJS/Runtime/runtimescene.ts",
"GDJS/package-lock.json",
"GDJS/package.json",
"GDJS/tests/karma.conf.js",
"GDJS/tests/package-lock.json",
"GDJS/tests/package.json",
"GDevelop.js/TestUtils/CodeGenerationHelpers.js",
"GDevelop.js/TestUtils/FakeAbstractFileSystem.js",
"GDevelop.js/__tests__/Core.js",
"GDevelop.js/__tests__/GDJS.js",
"GDevelop.js/__tests__/SerializerBenchmark.js",
"GDevelop.js/__tests__/Vector.js",
"GDevelop.js/package-lock.json",
"GDevelop.js/package.json",
"README.md",
"SharedLibs/ThreeAddons/.gitignore",
"SharedLibs/ThreeAddons/package.json",
"SharedLibs/TileMapHelper/.gitignore",
"SharedLibs/TileMapHelper/package.json",
"newIDE/app/package-lock.json",
"newIDE/app/package.json",
"newIDE/app/scripts/fix-codemod-syntax.py",
"newIDE/app/scripts/lib/ArrayHelpers.js",
"newIDE/app/scripts/lib/DownloadLocalFile.js",
"newIDE/app/scripts/submit-appx-to-microsoft-store.js",
"newIDE/app/scripts/test-portable-cli-export.js",
"newIDE/app/src/AiGeneration/AiConfiguration.js",
"newIDE/app/src/AiGeneration/AiRequestChat/ChatMessages.js",
"newIDE/app/src/AiGeneration/AiRequestChat/index.js",
"newIDE/app/src/AiGeneration/AiRequestContext.js",
"newIDE/app/src/AiGeneration/AskAiEditorContainer.js",
"newIDE/app/src/AiGeneration/AskAiStandAloneForm.js",
"newIDE/app/src/AiGeneration/PrepareAiUserContent.js",
"newIDE/app/src/AiGeneration/UseGenerateEvents.js",
"newIDE/app/src/AiGeneration/UseSearchAndInstallAsset.js",
"newIDE/app/src/AiGeneration/UseSearchAndInstallResource.js",
"newIDE/app/src/AiGeneration/Utils.js",
"newIDE/app/src/EditorFunctions/ScriptExecution/NonScriptableFunctionNames.js",
"newIDE/app/src/EditorFunctions/ScriptExecution/ScriptRunner.js",
"newIDE/app/src/EditorFunctions/ServerSideTools.spec.js",
"newIDE/app/src/EditorFunctions/index.js",
"newIDE/app/src/ExportAndShare/LocalExporters/LocalPreviewLauncher/index.js",
"newIDE/app/src/MainFrame/AboutDialog.js",
"newIDE/app/src/MainFrame/Changelog/ChangelogDialog.js",
"newIDE/app/src/MainFrame/Changelog/ChangelogRenderer.js",
"newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js",
"newIDE/app/src/MainFrame/Preferences/PreferencesContext.js",
"newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js",
"newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js",
"newIDE/app/src/Profile/AuthenticatedUserContext.js",
"newIDE/app/src/Profile/EditProfileDialog.js",
"newIDE/app/src/Profile/Subscription/SubscriptionChecker.js",
"newIDE/app/src/Profile/Subscription/SubscriptionContext.js",
"newIDE/app/src/ProjectManager/LoadingScreenEditor.js",
"newIDE/app/src/PropertiesEditor/index.js",
"newIDE/app/src/ResourcesEditor/ResourcePropertiesEditor/index.js",
"newIDE/app/src/ResourcesEditor/Toolbar.js",
"newIDE/app/src/ResourcesEditor/index.js",
"newIDE/app/src/ResourcesList/index.js",
"newIDE/app/src/UI/InlineCheckbox.js",
"newIDE/app/src/UI/List.js",
"newIDE/app/src/UI/SortableVirtualizedItemList/ItemRow.js",
"newIDE/app/src/UI/SortableVirtualizedItemList/index.js",
"newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js",
"newIDE/app/src/Utils/GDevelopServices/Authentication.js",
"newIDE/app/src/Utils/GDevelopServices/Generation.js",
"newIDE/app/src/Utils/GDevelopServices/Usage.js",
"newIDE/app/src/stories/componentStories/ResourcesList/ResourcesList.stories.js",
"newIDE/electron-app/app/main.js",
"newIDE/electron-app/app/package-lock.json",
"newIDE/electron-app/app/package.json",
"newIDE/electron-app/electron-builder-config.js",
"newIDE/electron-app/package-lock.json",
"newIDE/electron-app/package.json",
"newIDE/visual-tests/package-lock.json",
"newIDE/visual-tests/package.json",
"newIDE/web-app/package-lock.json",
"newIDE/web-app/package.json",
"scripts/README.md"
],
"forkOnly": [
".env.example",
".fallow.toml",
".github/codeql-config.yml",
".github/dependabot.yml",
".github/workflows/build-release.yml",
".github/workflows/ci.yml",
".github/workflows/fallow.yml",
".github/workflows/upstream-sync.yml",
".npmrc",
".sonarcloud.properties",
"AGENTS.md",
"AI_GAME_STUDIO_ASSESSMENT.md",
"BUILD.md",
"Extensions/3D/Model3DInstancePool.ts",
"Extensions/TileMap/tests/tilemapCulling.spec.js",
"GDJS/tests/tests-utils/assets/single-mesh.glb",
"GDJS/tests/tests/Extensions/3DCulling.js",
"GDJS/tests/tests/Extensions/Model3DInstancing.js",
"GDevelop.js/__tests__/Model3DObjectConfiguration.spec.js",
"KNOWN_VULNS.md",
"MAINTENANCE.md",
"SECRETS.md",
"SharedLibs/ThreeAddons/package-lock.json",
"SharedLibs/TileMapHelper/package-lock.json",
"newIDE/app/index.html",
"newIDE/app/src/AI/CustomAIClient.js",
"newIDE/app/src/AI/CustomAIClient.spec.js",
"newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.js",
"newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.spec.js",
"newIDE/app/src/AiGeneration/Studio/LoopGuard.js",
"newIDE/app/src/AiGeneration/Studio/LoopGuard.spec.js",
"newIDE/app/src/AiGeneration/Studio/PlanStore.js",
"newIDE/app/src/AiGeneration/Studio/PlanStore.spec.js",
"newIDE/app/src/AiGeneration/Studio/RequestWriteGate.js",
"newIDE/app/src/AiGeneration/Studio/RequestWriteGate.spec.js",
"newIDE/app/src/AiGeneration/Studio/Roles.js",
"newIDE/app/src/AiGeneration/Studio/Roles.spec.js",
"newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.js",
"newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.spec.js",
"newIDE/app/src/AiGeneration/Studio/UseStudioRuntime.js",
"newIDE/app/src/Utils/Array.ts",
"newIDE/app/src/Utils/Delay.ts",
"newIDE/app/src/Utils/GDevelopServices/ApiConfigs.spec.js",
"newIDE/app/src/Utils/UseForceUpdate.ts",
"newIDE/app/src/Utils/UseIsMounted.ts",
"newIDE/app/src/Utils/UseTimeout.ts",
"newIDE/app/tsconfig.json",
"newIDE/app/vite.config.ts",
"scripts/check-fork-divergence.js",
"scripts/security/check-lockfiles.js",
"scripts/sonarcloud-drift.js"
],
"upstreamMissing": [
"newIDE/app/patches/wavesurfer.js+7.12.6.patch",
"newIDE/app/src/Utils/Array.js",
"newIDE/app/src/Utils/Delay.js",
"newIDE/app/src/Utils/UseForceUpdate.js",
"newIDE/app/src/Utils/UseIsMounted.js",
"newIDE/app/src/Utils/UseTimeout.js",
"newIDE/web-app/yarn.lock"
]
}
@@ -1,2 +0,0 @@
// @flow
export * from '../AI/CustomAIClient';
@@ -206,14 +206,7 @@ const evaluateScript = ({
].join('\n');
// eslint-disable-next-line no-new-func
// nosonar: javascript:S1523 — `new Function` is intentional here. The
// script source comes from our own backend LLM (see file header). The
// shadowing of browser globals above is hygiene, not security: the
// script can do no more than the LLM itself can via tool calls.
// TODO(security): swap for a Worker / QuickJS-WASM sandbox when
// third-party prompts ever run through this code path.
// eslint-disable-next-line no-new-func
const scriptFunction: any = new Function(
const scriptFunction: any = new Function( // nosonar: javascript:S1523 — new Function is intentional: the script source comes from our own backend LLM, the browser globals it needs are shadowed above, and the script can do no more than the LLM already can via tool calls.
// $FlowFixMe[incompatible-type] - the Function constructor accepts parameter names then the body.
...[...parameterNames, functionBody]
);
@@ -221,9 +214,9 @@ const evaluateScript = ({
// Promise.resolve so that a synchronously thrown error is also converted
// into a rejected promise by the caller's try/catch on await.
// nosonar: javascript:S1523 — the scriptFunction is the result of
// `new Function` above; see the rationale comment there.
return Promise.resolve(scriptFunction(...scopedArguments));
return Promise.resolve(
scriptFunction(...scopedArguments) // nosonar: javascript:S1523 — invokes the function compiled by `new Function` above.
);
};
export const executeScript = async ({
+34 -3
View File
@@ -1,7 +1,38 @@
# Scripts files for GDevelop
- **ReleaseProcedure.bat**: this script compiles, generates documentation (see **GenerateAllDocs.bat**) and packages GDevelop for Windows in an installer and an archive.
- **ReleaseProcedure.sh**: compiles and packages GD for Ubuntu (see _Binaries/Packaging_).
- **CopyWindowsToLinuxReleaseFiles.sh**: Copies all files in _Binaries/Output/Release_Windows_ to _Binaries/Output/Release_Linux_. Call it after any change in _Binaries/Output/Release_Windows_.
Repository-wide tooling. Per-package scripts live in each package's own
`package.json`, not here.
## Fork maintenance
See [MAINTENANCE.md](../MAINTENANCE.md) for how these fit together.
- **check-fork-divergence.js**: keeps the fork's divergence from upstream
`4ian/GDevelop` bounded. Compares this tree against upstream and checks the
result against the checked-in `fork-divergence.json` allowlist; exits 1 on a
path that diverges without being listed or a listed path that no longer
diverges. `--update` rewrites the allowlist, `--list` prints the divergence
set. Runs in `ci.yml` (`fork-divergence` job) and, informationally, in
`upstream-sync.yml`.
- **sonarcloud-drift.js**: compares the project's open SonarCloud issue count
against `.sonarcloud-drift-baseline` and fails when growth exceeds the
threshold. Self-seeds the baseline when it is missing and writes `count` /
`baseline` / `seeded` to `$GITHUB_OUTPUT`. `--update-baseline` records the
current count; `--max-growth-pct=<n>` overrides the 10% default. Runs weekly
in `.github/workflows/fallow.yml`.
- **security/check-lockfiles.js**: asserts no manifest in the repo pins a
known-malicious or critically vulnerable version of the packages listed in
the script. Runs in `ci.yml` (`lockfile-guard` job).
## Documentation and translations
- **GenerateAllDocs.[bat|sh]**: Calls doxygen to generate all documentation into _docs_ folder.
- **ExtractTranslations.[bat|sh]**: Creates the _source.pot_ file containing the strings to be translated using [Crowdin](https://crowdin.com/project/gdevelop).
## Upstream scripts removed in this fork
`ReleaseProcedure.bat`, `ReleaseProcedure.sh` and
`CopyWindowsToLinuxReleaseFiles.sh` are referenced by older upstream docs but do
not exist in this checkout — packaging happens through
`.github/workflows/build-release.yml` and `newIDE/electron-app`. Do not add them
back from an upstream sync.
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env node
/*
* Fork-divergence guard.
*
* This fork tracks upstream 4ian/GDevelop. Divergence is intentional in a
* handful of places (BYOK/local-AI, unlocked client features, the Heretek
* update feed, the fork's CI) and accidental in others. Accidental divergence
* is what makes upstream syncs expensive: the fork must re-resolve it on every
* merge, forever.
*
* This script makes the divergence set explicit and bounded:
*
* * `fork-divergence.json` is the checked-in allowlist of every path the
* fork intentionally diverges on, in three buckets — `modified`,
* `forkOnly`, `upstreamMissing`.
* * The script recomputes the divergence set from git + the upstream tree
* and fails when the two disagree in either direction:
* - a path diverges that is not in the allowlist -> newly-introduced
* divergence, which must be a deliberate decision;
* - an allowlisted path no longer diverges -> stale entry, which
* must be removed so the file stays trustworthy.
*
* Usage:
* node scripts/check-fork-divergence.js # verify (CI + local)
* node scripts/check-fork-divergence.js --update # rewrite the allowlist
* node scripts/check-fork-divergence.js --list # print the diff, exit 0
*
* Upstream tree lookup, in order of preference:
* 1. `git ls-tree` of a local upstream ref (`upstream/master`, then the
* `origin/upstream-sync-*` branches the sync workflow pushes), when one
* exists and is reachable;
* 2. the GitHub trees API (`GH_TOKEN` / `GITHUB_TOKEN`), which needs no
* clone of the ~1.7 GB upstream repository.
*
* Exit codes: 0 clean, 1 divergence mismatch, 2 fatal error.
*/
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const REPO_ROOT = path.join(__dirname, '..');
const MANIFEST_NAME = 'fork-divergence.json';
const MANIFEST_PATH = path.join(REPO_ROOT, MANIFEST_NAME);
const UPSTREAM_REPO = '4ian/GDevelop';
const UPSTREAM_REF = 'master';
function git(...args) {
return execFileSync('git', args, {
cwd: REPO_ROOT,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
});
}
/** Local tree of HEAD as { path: blobSha }. */
function headTree() {
const out = git('ls-tree', '-r', '-z', 'HEAD');
const map = new Map();
for (const entry of out.split('\0')) {
if (!entry) continue;
// "<mode> <type> <sha>\t<path>"
const tab = entry.indexOf('\t');
if (tab === -1) continue;
const meta = entry.slice(0, tab).split(' ');
const sha = meta[2];
const filePath = entry.slice(tab + 1);
if (meta[1] === 'blob') map.set(filePath, sha);
}
return map;
}
/**
* Local tree including uncommitted work, as { path: blobSha }.
*
* Starting from the HEAD tree and patching in only the paths that actually
* differ keeps this cheap: `git hash-object` runs once per changed file, not
* once per tracked file. Without this, `--update` would silently ignore
* everything still sitting in the working tree and the committed manifest
* would disagree with the tree it is supposed to describe.
*/
function localTree() {
const map = headTree();
// Tracked files whose content differs from HEAD (staged or unstaged).
const dirty = git('diff-index', '--name-only', '-z', 'HEAD').split('\0').filter(Boolean);
const deleted = new Set(
git('ls-files', '--deleted', '-z').split('\0').filter(Boolean)
);
for (const filePath of dirty) {
if (deleted.has(filePath)) {
map.delete(filePath);
continue;
}
map.set(filePath, git('hash-object', '--', filePath).trim());
}
// Untracked files that are not ignored.
const untracked = git('ls-files', '--others', '--exclude-standard', '-z')
.split('\0')
.filter(Boolean);
for (const filePath of untracked) {
map.set(filePath, git('hash-object', '--', filePath).trim());
}
return map;
}
/** Upstream tree as { path: blobSha }, via a local ref when possible. */
function upstreamTree() {
const candidates = ['refs/remotes/upstream/master', 'refs/heads/upstream/master'];
for (const ref of candidates) {
try {
git('rev-parse', '--verify', '--quiet', ref);
} catch (_) {
continue;
}
const out = git('ls-tree', '-r', '-z', ref);
const map = new Map();
for (const entry of out.split('\0')) {
if (!entry) continue;
const tab = entry.indexOf('\t');
if (tab === -1) continue;
const meta = entry.slice(0, tab).split(' ');
if (meta[1] === 'blob') map.set(entry.slice(tab + 1), meta[2]);
}
return { source: ref, tree: map };
}
// Fall back to the GitHub trees API. No upstream clone required.
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
if (!token) {
throw new Error(
'No local upstream ref and no GH_TOKEN/GITHUB_TOKEN. Either run\n' +
' git remote add upstream https://github.com/4ian/GDevelop.git\n' +
' git fetch --depth=1 upstream master\n' +
'or export GH_TOKEN.'
);
}
const url = `https://api.github.com/repos/${UPSTREAM_REPO}/git/trees/${UPSTREAM_REF}?recursive=1`;
const body = execFileSync(
'curl',
['-fsSL', '-H', `Authorization: Bearer ${token}`, '-H', 'Accept: application/vnd.github+json', url],
{ encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 }
);
const json = JSON.parse(body);
if (json.truncated) {
throw new Error(
`Upstream tree listing was truncated by the GitHub API; use a local upstream ref instead.`
);
}
const map = new Map();
for (const entry of json.tree) {
if (entry.type === 'blob') map.set(entry.path, entry.sha);
}
return { source: `api:${UPSTREAM_REPO}@${UPSTREAM_REF}`, tree: map };
}
function computeDivergence() {
const local = localTree();
const { source, tree: upstream } = upstreamTree();
const modified = [];
const forkOnly = [];
const upstreamMissing = [];
// The manifest cannot describe itself: its content changes whenever the
// divergence set changes, so its hash is never stable. It is excluded from
// both sides. `.sonarcloud-drift-baseline` is excluded for the same reason —
// it records the live issue count, which moves independently of the tree.
const selfReferential = new Set([MANIFEST_NAME, '.sonarcloud-drift-baseline']);
for (const [filePath, sha] of local) {
if (selfReferential.has(filePath)) continue;
if (!upstream.has(filePath)) forkOnly.push(filePath);
else if (upstream.get(filePath) !== sha) modified.push(filePath);
}
for (const filePath of upstream.keys()) {
if (selfReferential.has(filePath)) continue;
if (!local.has(filePath)) upstreamMissing.push(filePath);
}
modified.sort();
forkOnly.sort();
upstreamMissing.sort();
return { source, modified, forkOnly, upstreamMissing };
}
function readManifest() {
if (!fs.existsSync(MANIFEST_PATH)) return null;
return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
}
function toSet(raw) {
return new Set(raw || []);
}
function diffSets(actual, allowed) {
const unexpected = [];
const stale = [];
for (const value of actual) if (!allowed.has(value)) unexpected.push(value);
for (const value of allowed) if (!actual.has(value)) stale.push(value);
return { unexpected: unexpected.sort(), stale: stale.sort() };
}
function main() {
const argv = process.argv.slice(2);
const update = argv.includes('--update');
const list = argv.includes('--list');
const actual = computeDivergence();
if (list) {
console.log(`Upstream source: ${actual.source}`);
console.log(`modified: ${actual.modified.length}`);
console.log(`forkOnly: ${actual.forkOnly.length}`);
console.log(`upstreamMissing: ${actual.upstreamMissing.length}`);
for (const bucket of ['modified', 'forkOnly', 'upstreamMissing']) {
for (const filePath of actual[bucket]) console.log(` ${bucket} ${filePath}`);
}
return;
}
if (update) {
const previous = readManifest();
const manifest = {
$comment:
'Checked-in allowlist of every path this fork intentionally diverges from ' +
'4ian/GDevelop on. Regenerate with `node scripts/check-fork-divergence.js --update`. ' +
'Enforced by `node scripts/check-fork-divergence.js` in CI. See MAINTENANCE.md.',
updatedAt: new Date().toISOString().slice(0, 10),
upstreamSource: actual.source,
modified: actual.modified,
forkOnly: actual.forkOnly,
upstreamMissing: actual.upstreamMissing,
};
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
if (previous) {
const counts = {};
for (const bucket of ['modified', 'forkOnly', 'upstreamMissing']) {
const delta = actual[bucket].length - (previous[bucket] || []).length;
if (delta !== 0) counts[bucket] = delta > 0 ? `+${delta}` : `${delta}`;
}
console.log(
`Wrote ${path.relative(REPO_ROOT, MANIFEST_PATH)} ` +
`(${actual.modified.length} modified, ${actual.forkOnly.length} fork-only, ` +
`${actual.upstreamMissing.length} upstream-missing)` +
(Object.keys(counts).length ? ` delta vs previous: ${JSON.stringify(counts)}` : '')
);
} else {
console.log(`Wrote ${path.relative(REPO_ROOT, MANIFEST_PATH)}`);
}
return;
}
const manifest = readManifest();
if (!manifest) {
console.error(
`No ${path.relative(REPO_ROOT, MANIFEST_PATH)}. Create it with:\n` +
` node scripts/check-fork-divergence.js --update`
);
process.exit(1);
}
console.log(`Upstream source: ${actual.source}`);
const buckets = ['modified', 'forkOnly', 'upstreamMissing'];
let failed = false;
for (const bucket of buckets) {
const { unexpected, stale } = diffSets(
toSet(actual[bucket]),
toSet(manifest[bucket])
);
console.log(
`${bucket}: ${actual[bucket].length} (allowlisted ${(manifest[bucket] || []).length})`
);
if (unexpected.length) {
failed = true;
console.error(` UNEXPECTED divergence (${unexpected.length}) — not in the allowlist:`);
for (const filePath of unexpected.slice(0, 40)) console.error(` ${filePath}`);
if (unexpected.length > 40) {
console.error(` … and ${unexpected.length - 40} more`);
}
}
if (stale.length) {
failed = true;
console.error(` STALE allowlist entries (${stale.length}) — no longer diverge:`);
for (const filePath of stale.slice(0, 40)) console.error(` ${filePath}`);
if (stale.length > 40) {
console.error(` … and ${stale.length - 40} more`);
}
}
}
if (failed) {
console.error(
'\nDivergence from upstream changed. Two acceptable resolutions:\n' +
' 1. The new divergence is intended -> record it:\n' +
' node scripts/check-fork-divergence.js --update && git add fork-divergence.json\n' +
' 2. It was accidental -> revert it, so the file stays in sync with upstream.'
);
process.exit(1);
}
console.log('Fork divergence matches fork-divergence.json.');
}
try {
main();
} catch (error) {
console.error(error.stack || error.message || error);
process.exit(2);
}
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env bash
# SonarCloud suppressions setup
#
# After this PR is merged, the operator (you) must paste the multicriteria
# block from .sonarcloud.properties into SonarCloud so the open-issue
# count drops. This script is a no-op run helper that prints the
# steps and the relevant block locations.
#
# Usage:
# bash scripts/sonarcloud-apply-suppressions.sh
#
# After running, complete the steps printed to the console, then verify
# with:
# node scripts/sonarcloud-drift.js --update-baseline
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROPS="$REPO_ROOT/.sonarcloud.properties"
cat <<EOF
=== SonarCloud suppressions setup ===
This script is a checklist. Apply each step in SonarCloud's web UI.
Step 1 — Path exclusions
Open: https://sonarcloud.io/project/settings?category=scope&id=Heretek-AI_GDevelop-Heretek
Section: "Analysis Scope" → "Source File Exclusions"
Paste (as a comma-separated list, all on one line):
$(awk '/^sonar.exclusions=/,/^$/' "$PROPS" | head -50 | tr -d '\\\n' | sed 's/^sonar.exclusions=//' | sed 's/^/ /')
Step 2 — Per-rule multicriteria suppressions
Open: https://sonarcloud.io/project/settings?category=exclusions&id=Heretek-AI_GDevelop-Heretek
Section: "Issues Exclusions" → "Multi-criteria" → "Add"
The criteria to add are:
$(awk '
/^[[:space:]]*#?[[:space:]]*sonar\.issue\.ignore\.multicriteria\.e[0-9]+\.(ruleKey|resourceKey)=/ {
line = $0
sub(/^[[:space:]]*#?[[:space:]]*/, "", line)
print " " line
}
' "$PROPS")
Step 3 — Verify
After pasting, run:
node scripts/sonarcloud-drift.js --update-baseline
This records the current count as the new baseline so subsequent
drift checks compare against it.
Step 4 — Watch the result
The expected drop is from ~3,029 open (pre-suppressions) to ~500-1,000
(post-suppressions). If you see a smaller drop, check the
SonarCloud audit log for any entries that failed to import.
EOF
+45 -16
View File
@@ -14,6 +14,15 @@
* node scripts/sonarcloud-drift.js # report current count
* node scripts/sonarcloud-drift.js --update-baseline # record new baseline
* node scripts/sonarcloud-drift.js --max-growth-pct=10 # custom threshold
*
* CI contract (consumed by .github/workflows/fallow.yml):
* exits 0 and writes `count=<n>` + `baseline=<n|-> to $GITHUB_OUTPUT
* exits 1 when growth exceeds the threshold
* exits 2 on a fatal error (network/parse)
*
* The baseline file is committed. When it is missing, this script seeds it
* from the current count and records that it did so in `seeded=true`, so a
* fresh checkout never fails the first run for lack of a baseline.
*/
const fs = require('fs');
@@ -38,6 +47,13 @@ function parseArgs(argv) {
return args;
}
function writeGithubOutput(fields) {
const outputPath = process.env.GITHUB_OUTPUT;
if (!outputPath) return;
const lines = Object.keys(fields).map(key => `${key}=${fields[key]}`);
fs.appendFileSync(outputPath, lines.join('\n') + '\n');
}
function fetchOpenCount() {
return new Promise((resolve, reject) => {
const url = `https://sonarcloud.io/api/issues/search?componentKeys=${PROJECT_KEY}&issueStatuses=OPEN&ps=1`;
@@ -96,27 +112,40 @@ async function main() {
if (args.updateBaseline) {
writeBaseline(count);
console.log(`Wrote baseline to ${BASELINE_PATH}`);
writeGithubOutput({ count, baseline: count, seeded: 'true' });
return;
}
if (baseline) {
const baseCount = baseline.count;
const delta = count - baseCount;
const pct = ((delta / baseCount) * 100).toFixed(2);
console.log(`Baseline: ${baseCount} (recorded ${baseline.recordedAt})`);
console.log(`Delta: ${delta > 0 ? '+' : ''}${delta} (${pct}%)`);
if (delta > 0 && parseFloat(pct) > args.maxGrowthPct) {
console.error(
`\nERROR: open-issue count grew by ${pct}% (>${args.maxGrowthPct}% threshold)`
);
console.error(` See SONARCLOUD_REMEDIATION_PLAN.md for suppressions.`);
process.exit(1);
}
} else {
// No baseline committed yet (fresh checkout or first run after a reset):
// seed it rather than failing, and say so loudly so it gets committed.
if (!baseline) {
writeBaseline(count);
console.log(
`No baseline found at ${BASELINE_PATH}; skipping growth check.`
`No baseline found at ${BASELINE_PATH}; seeded it with ${count}.`
);
console.log(`Run with --update-baseline to record the current count.`);
console.log(
`Commit .sonarcloud-drift-baseline so subsequent runs compare against it.`
);
writeGithubOutput({ count, baseline: count, seeded: 'true' });
return;
}
const baseCount = baseline.count;
const delta = count - baseCount;
const pct = ((delta / baseCount) * 100).toFixed(2);
console.log(`Baseline: ${baseCount} (recorded ${baseline.recordedAt})`);
console.log(`Delta: ${delta > 0 ? '+' : ''}${delta} (${pct}%)`);
writeGithubOutput({ count, baseline: baseCount, seeded: 'false' });
if (delta > 0 && parseFloat(pct) > args.maxGrowthPct) {
console.error(
`\nERROR: open-issue count grew by ${pct}% (>${args.maxGrowthPct}% threshold)`
);
console.error(
` See MAINTENANCE.md ("Static analysis") before relaxing this.`
);
process.exit(1);
}
}