@invarn/cibuild 2.9.5 → 2.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-35SI42CD.mjs +1 -0
- package/dist/chunk-IC45KVLJ.mjs +1 -0
- package/dist/chunk-XDY4V23V.mjs +1 -0
- package/dist/cli.cjs +14 -14
- package/dist/commands.mjs +1 -0
- package/dist/lib.mjs +3934 -0
- package/package.json +6 -10
- package/dist/src/cli.d.ts +0 -3
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -399
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts +0 -38
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +0 -1
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +0 -222
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +0 -41
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +0 -1
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +0 -324
- package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts +0 -2
- package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts.map +0 -1
- package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.js +0 -382
- package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.d.ts +0 -29
- package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.d.ts.map +0 -1
- package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.js +0 -160
- package/dist/src/commands/android-gms-guard.test.d.ts +0 -9
- package/dist/src/commands/android-gms-guard.test.d.ts.map +0 -1
- package/dist/src/commands/android-gms-guard.test.js +0 -78
- package/dist/src/commands/android-java-version.test.d.ts +0 -12
- package/dist/src/commands/android-java-version.test.d.ts.map +0 -1
- package/dist/src/commands/android-java-version.test.js +0 -209
- package/dist/src/commands/android-scanner-java.test.d.ts +0 -2
- package/dist/src/commands/android-scanner-java.test.d.ts.map +0 -1
- package/dist/src/commands/android-scanner-java.test.js +0 -130
- package/dist/src/commands/android-scanner.d.ts +0 -250
- package/dist/src/commands/android-scanner.d.ts.map +0 -1
- package/dist/src/commands/android-scanner.js +0 -1639
- package/dist/src/commands/build.d.ts +0 -215
- package/dist/src/commands/build.d.ts.map +0 -1
- package/dist/src/commands/build.js +0 -1433
- package/dist/src/commands/build.test.d.ts +0 -13
- package/dist/src/commands/build.test.d.ts.map +0 -1
- package/dist/src/commands/build.test.js +0 -121
- package/dist/src/commands/detect-platform.d.ts +0 -13
- package/dist/src/commands/detect-platform.d.ts.map +0 -1
- package/dist/src/commands/detect-platform.js +0 -51
- package/dist/src/commands/detect-project.d.ts +0 -7
- package/dist/src/commands/detect-project.d.ts.map +0 -1
- package/dist/src/commands/detect-project.js +0 -12
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +0 -26
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +0 -1
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +0 -196
- package/dist/src/commands/edit.d.ts +0 -12
- package/dist/src/commands/edit.d.ts.map +0 -1
- package/dist/src/commands/edit.js +0 -652
- package/dist/src/commands/edit.test.d.ts +0 -6
- package/dist/src/commands/edit.test.d.ts.map +0 -1
- package/dist/src/commands/edit.test.js +0 -45
- package/dist/src/commands/fidelity-trim.d.ts +0 -13
- package/dist/src/commands/fidelity-trim.d.ts.map +0 -1
- package/dist/src/commands/fidelity-trim.js +0 -21
- package/dist/src/commands/fidelity-trim.test.d.ts +0 -8
- package/dist/src/commands/fidelity-trim.test.d.ts.map +0 -1
- package/dist/src/commands/fidelity-trim.test.js +0 -79
- package/dist/src/commands/file-secret-collector.d.ts +0 -50
- package/dist/src/commands/file-secret-collector.d.ts.map +0 -1
- package/dist/src/commands/file-secret-collector.js +0 -217
- package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.d.ts +0 -38
- package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.d.ts.map +0 -1
- package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.js +0 -300
- package/dist/src/commands/github-workflow.d.ts +0 -5
- package/dist/src/commands/github-workflow.d.ts.map +0 -1
- package/dist/src/commands/github-workflow.js +0 -45
- package/dist/src/commands/index.d.ts +0 -33
- package/dist/src/commands/index.d.ts.map +0 -1
- package/dist/src/commands/index.js +0 -33
- package/dist/src/commands/init.d.ts +0 -20
- package/dist/src/commands/init.d.ts.map +0 -1
- package/dist/src/commands/init.js +0 -264
- package/dist/src/commands/ios-scanner.d.ts +0 -104
- package/dist/src/commands/ios-scanner.d.ts.map +0 -1
- package/dist/src/commands/ios-scanner.js +0 -492
- package/dist/src/commands/ios-scheme-ranking.test.d.ts +0 -2
- package/dist/src/commands/ios-scheme-ranking.test.d.ts.map +0 -1
- package/dist/src/commands/ios-scheme-ranking.test.js +0 -264
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts +0 -30
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts.map +0 -1
- package/dist/src/commands/local-properties-reaches-every-workflow.test.js +0 -146
- package/dist/src/commands/reset.d.ts +0 -7
- package/dist/src/commands/reset.d.ts.map +0 -1
- package/dist/src/commands/reset.js +0 -81
- package/dist/src/commands/run.d.ts +0 -22
- package/dist/src/commands/run.d.ts.map +0 -1
- package/dist/src/commands/run.js +0 -141
- package/dist/src/commands/secrets-sync-workflow.d.ts +0 -15
- package/dist/src/commands/secrets-sync-workflow.d.ts.map +0 -1
- package/dist/src/commands/secrets-sync-workflow.js +0 -255
- package/dist/src/commands/secrets-upload.d.ts +0 -21
- package/dist/src/commands/secrets-upload.d.ts.map +0 -1
- package/dist/src/commands/secrets-upload.js +0 -177
- package/dist/src/commands/secrets-upload.test.d.ts +0 -5
- package/dist/src/commands/secrets-upload.test.d.ts.map +0 -1
- package/dist/src/commands/secrets-upload.test.js +0 -60
- package/dist/src/commands/validate.d.ts +0 -10
- package/dist/src/commands/validate.d.ts.map +0 -1
- package/dist/src/commands/validate.js +0 -46
- package/dist/src/config.d.ts +0 -3
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js +0 -47
- package/dist/src/envman/cli.d.ts +0 -21
- package/dist/src/envman/cli.d.ts.map +0 -1
- package/dist/src/envman/cli.js +0 -240
- package/dist/src/envman/envman.d.ts +0 -83
- package/dist/src/envman/envman.d.ts.map +0 -1
- package/dist/src/envman/envman.js +0 -361
- package/dist/src/envman/envman.test.d.ts +0 -5
- package/dist/src/envman/envman.test.d.ts.map +0 -1
- package/dist/src/envman/envman.test.js +0 -236
- package/dist/src/envman/envstore-round-trip.test.d.ts +0 -27
- package/dist/src/envman/envstore-round-trip.test.d.ts.map +0 -1
- package/dist/src/envman/envstore-round-trip.test.js +0 -130
- package/dist/src/envman/index.d.ts +0 -23
- package/dist/src/envman/index.d.ts.map +0 -1
- package/dist/src/envman/index.js +0 -23
- package/dist/src/envman/shim.d.ts +0 -16
- package/dist/src/envman/shim.d.ts.map +0 -1
- package/dist/src/envman/shim.js +0 -66
- package/dist/src/envman/types.d.ts +0 -55
- package/dist/src/envman/types.d.ts.map +0 -1
- package/dist/src/envman/types.js +0 -12
- package/dist/src/lib.d.ts +0 -33
- package/dist/src/lib.d.ts.map +0 -1
- package/dist/src/lib.js +0 -37
- package/dist/src/pipeline.d.ts +0 -3
- package/dist/src/pipeline.d.ts.map +0 -1
- package/dist/src/pipeline.js +0 -57
- package/dist/src/runner-node-step.test.d.ts +0 -12
- package/dist/src/runner-node-step.test.d.ts.map +0 -1
- package/dist/src/runner-node-step.test.js +0 -61
- package/dist/src/runner.d.ts +0 -31
- package/dist/src/runner.d.ts.map +0 -1
- package/dist/src/runner.js +0 -240
- package/dist/src/shared/detect-project.d.ts +0 -57
- package/dist/src/shared/detect-project.d.ts.map +0 -1
- package/dist/src/shared/detect-project.js +0 -219
- package/dist/src/shared/detect-project.test.d.ts +0 -2
- package/dist/src/shared/detect-project.test.d.ts.map +0 -1
- package/dist/src/shared/detect-project.test.js +0 -161
- package/dist/src/shared/git.d.ts +0 -25
- package/dist/src/shared/git.d.ts.map +0 -1
- package/dist/src/shared/git.js +0 -53
- package/dist/src/shared/git.test.d.ts +0 -9
- package/dist/src/shared/git.test.d.ts.map +0 -1
- package/dist/src/shared/git.test.js +0 -66
- package/dist/src/shared/gitignore.d.ts +0 -6
- package/dist/src/shared/gitignore.d.ts.map +0 -1
- package/dist/src/shared/gitignore.js +0 -26
- package/dist/src/shared/gradle-settings.d.ts +0 -49
- package/dist/src/shared/gradle-settings.d.ts.map +0 -1
- package/dist/src/shared/gradle-settings.js +0 -138
- package/dist/src/shared/gradle-settings.test.d.ts +0 -2
- package/dist/src/shared/gradle-settings.test.d.ts.map +0 -1
- package/dist/src/shared/gradle-settings.test.js +0 -65
- package/dist/src/shared/prompts.d.ts +0 -30
- package/dist/src/shared/prompts.d.ts.map +0 -1
- package/dist/src/shared/prompts.js +0 -121
- package/dist/src/shared/xcode-container.test.d.ts +0 -2
- package/dist/src/shared/xcode-container.test.d.ts.map +0 -1
- package/dist/src/shared/xcode-container.test.js +0 -123
- package/dist/src/step-marker.d.ts +0 -12
- package/dist/src/step-marker.d.ts.map +0 -1
- package/dist/src/step-marker.js +0 -29
- package/dist/src/step-marker.test.d.ts +0 -11
- package/dist/src/step-marker.test.d.ts.map +0 -1
- package/dist/src/step-marker.test.js +0 -45
- package/dist/src/types.d.ts +0 -58
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -2
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +0 -28
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +0 -1
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +0 -223
- package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.d.ts +0 -20
- package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.d.ts.map +0 -1
- package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.js +0 -201
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +0 -37
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +0 -1
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +0 -255
- package/dist/src/yaml/a-missing-credential-is-loud.test.d.ts +0 -32
- package/dist/src/yaml/a-missing-credential-is-loud.test.d.ts.map +0 -1
- package/dist/src/yaml/a-missing-credential-is-loud.test.js +0 -301
- package/dist/src/yaml/a-runtime-requirement-names-a-real-provider.test.d.ts +0 -18
- package/dist/src/yaml/a-runtime-requirement-names-a-real-provider.test.d.ts.map +0 -1
- package/dist/src/yaml/a-runtime-requirement-names-a-real-provider.test.js +0 -66
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +0 -34
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +0 -1
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +0 -218
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +0 -27
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +0 -1
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +0 -126
- package/dist/src/yaml/bitrise-compat.d.ts +0 -65
- package/dist/src/yaml/bitrise-compat.d.ts.map +0 -1
- package/dist/src/yaml/bitrise-compat.js +0 -206
- package/dist/src/yaml/bitrise-compat.test.d.ts +0 -5
- package/dist/src/yaml/bitrise-compat.test.d.ts.map +0 -1
- package/dist/src/yaml/bitrise-compat.test.js +0 -347
- package/dist/src/yaml/cache-key-missing-file.test.d.ts +0 -12
- package/dist/src/yaml/cache-key-missing-file.test.d.ts.map +0 -1
- package/dist/src/yaml/cache-key-missing-file.test.js +0 -116
- package/dist/src/yaml/converter.d.ts +0 -103
- package/dist/src/yaml/converter.d.ts.map +0 -1
- package/dist/src/yaml/converter.js +0 -420
- package/dist/src/yaml/converter.test.d.ts +0 -5
- package/dist/src/yaml/converter.test.d.ts.map +0 -1
- package/dist/src/yaml/converter.test.js +0 -548
- package/dist/src/yaml/e2e.test.d.ts +0 -6
- package/dist/src/yaml/e2e.test.d.ts.map +0 -1
- package/dist/src/yaml/e2e.test.js +0 -446
- package/dist/src/yaml/env-resolver.d.ts +0 -218
- package/dist/src/yaml/env-resolver.d.ts.map +0 -1
- package/dist/src/yaml/env-resolver.js +0 -591
- package/dist/src/yaml/env-resolver.test.d.ts +0 -5
- package/dist/src/yaml/env-resolver.test.d.ts.map +0 -1
- package/dist/src/yaml/env-resolver.test.js +0 -502
- package/dist/src/yaml/every-build-log-witnesses-the-commit-on-disk.test.d.ts +0 -16
- package/dist/src/yaml/every-build-log-witnesses-the-commit-on-disk.test.d.ts.map +0 -1
- package/dist/src/yaml/every-build-log-witnesses-the-commit-on-disk.test.js +0 -126
- package/dist/src/yaml/fidelity-input-digest.d.ts +0 -37
- package/dist/src/yaml/fidelity-input-digest.d.ts.map +0 -1
- package/dist/src/yaml/fidelity-input-digest.js +0 -208
- package/dist/src/yaml/fidelity-input-digest.test.d.ts +0 -2
- package/dist/src/yaml/fidelity-input-digest.test.d.ts.map +0 -1
- package/dist/src/yaml/fidelity-input-digest.test.js +0 -167
- package/dist/src/yaml/fidelity-manifest.d.ts +0 -126
- package/dist/src/yaml/fidelity-manifest.d.ts.map +0 -1
- package/dist/src/yaml/fidelity-manifest.js +0 -134
- package/dist/src/yaml/fidelity-manifest.test.d.ts +0 -12
- package/dist/src/yaml/fidelity-manifest.test.d.ts.map +0 -1
- package/dist/src/yaml/fidelity-manifest.test.js +0 -251
- package/dist/src/yaml/interactive-prompts.d.ts +0 -71
- package/dist/src/yaml/interactive-prompts.d.ts.map +0 -1
- package/dist/src/yaml/interactive-prompts.js +0 -260
- package/dist/src/yaml/meta-helpers.d.ts +0 -28
- package/dist/src/yaml/meta-helpers.d.ts.map +0 -1
- package/dist/src/yaml/meta-helpers.js +0 -46
- package/dist/src/yaml/missing-env-handler.d.ts +0 -45
- package/dist/src/yaml/missing-env-handler.d.ts.map +0 -1
- package/dist/src/yaml/missing-env-handler.js +0 -64
- package/dist/src/yaml/parser.d.ts +0 -33
- package/dist/src/yaml/parser.d.ts.map +0 -1
- package/dist/src/yaml/parser.js +0 -148
- package/dist/src/yaml/parser.test.d.ts +0 -5
- package/dist/src/yaml/parser.test.d.ts.map +0 -1
- package/dist/src/yaml/parser.test.js +0 -83
- package/dist/src/yaml/pipeline-with-secrets.d.ts +0 -25
- package/dist/src/yaml/pipeline-with-secrets.d.ts.map +0 -1
- package/dist/src/yaml/pipeline-with-secrets.js +0 -76
- package/dist/src/yaml/platform-detector.d.ts +0 -83
- package/dist/src/yaml/platform-detector.d.ts.map +0 -1
- package/dist/src/yaml/platform-detector.js +0 -189
- package/dist/src/yaml/platform-detector.test.d.ts +0 -5
- package/dist/src/yaml/platform-detector.test.d.ts.map +0 -1
- package/dist/src/yaml/platform-detector.test.js +0 -525
- package/dist/src/yaml/preflight-validation.d.ts +0 -29
- package/dist/src/yaml/preflight-validation.d.ts.map +0 -1
- package/dist/src/yaml/preflight-validation.js +0 -21
- package/dist/src/yaml/secrets-manager.d.ts +0 -87
- package/dist/src/yaml/secrets-manager.d.ts.map +0 -1
- package/dist/src/yaml/secrets-manager.js +0 -230
- package/dist/src/yaml/shell-assignments.d.ts +0 -40
- package/dist/src/yaml/shell-assignments.d.ts.map +0 -1
- package/dist/src/yaml/shell-assignments.js +0 -77
- package/dist/src/yaml/step-validator-check-failure.test.d.ts +0 -24
- package/dist/src/yaml/step-validator-check-failure.test.d.ts.map +0 -1
- package/dist/src/yaml/step-validator-check-failure.test.js +0 -130
- package/dist/src/yaml/step-validator-self-assigned.test.d.ts +0 -23
- package/dist/src/yaml/step-validator-self-assigned.test.d.ts.map +0 -1
- package/dist/src/yaml/step-validator-self-assigned.test.js +0 -191
- package/dist/src/yaml/step-validator.d.ts +0 -67
- package/dist/src/yaml/step-validator.d.ts.map +0 -1
- package/dist/src/yaml/step-validator.js +0 -466
- package/dist/src/yaml/step-validator.test.d.ts +0 -18
- package/dist/src/yaml/step-validator.test.d.ts.map +0 -1
- package/dist/src/yaml/step-validator.test.js +0 -140
- package/dist/src/yaml/steps/android-sign.d.ts +0 -35
- package/dist/src/yaml/steps/android-sign.d.ts.map +0 -1
- package/dist/src/yaml/steps/android-sign.js +0 -147
- package/dist/src/yaml/steps/android-variant-resolution.test.d.ts +0 -2
- package/dist/src/yaml/steps/android-variant-resolution.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/android-variant-resolution.test.js +0 -260
- package/dist/src/yaml/steps/android-version.d.ts +0 -26
- package/dist/src/yaml/steps/android-version.d.ts.map +0 -1
- package/dist/src/yaml/steps/android-version.js +0 -128
- package/dist/src/yaml/steps/android-version.test.d.ts +0 -5
- package/dist/src/yaml/steps/android-version.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/android-version.test.js +0 -196
- package/dist/src/yaml/steps/android.d.ts +0 -156
- package/dist/src/yaml/steps/android.d.ts.map +0 -1
- package/dist/src/yaml/steps/android.js +0 -1092
- package/dist/src/yaml/steps/app-store-deploy.d.ts +0 -48
- package/dist/src/yaml/steps/app-store-deploy.d.ts.map +0 -1
- package/dist/src/yaml/steps/app-store-deploy.js +0 -167
- package/dist/src/yaml/steps/base.d.ts +0 -263
- package/dist/src/yaml/steps/base.d.ts.map +0 -1
- package/dist/src/yaml/steps/base.js +0 -380
- package/dist/src/yaml/steps/bitrise-android-tools.d.ts +0 -26
- package/dist/src/yaml/steps/bitrise-android-tools.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-android-tools.js +0 -200
- package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts +0 -5
- package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-android-tools.test.js +0 -280
- package/dist/src/yaml/steps/bitrise-apk-info.d.ts +0 -22
- package/dist/src/yaml/steps/bitrise-apk-info.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-apk-info.js +0 -144
- package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts +0 -5
- package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-apk-info.test.js +0 -331
- package/dist/src/yaml/steps/bitrise-slack.d.ts +0 -49
- package/dist/src/yaml/steps/bitrise-slack.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-slack.js +0 -280
- package/dist/src/yaml/steps/bitrise-slack.test.d.ts +0 -5
- package/dist/src/yaml/steps/bitrise-slack.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-slack.test.js +0 -484
- package/dist/src/yaml/steps/bitrise-ssh.d.ts +0 -27
- package/dist/src/yaml/steps/bitrise-ssh.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-ssh.js +0 -134
- package/dist/src/yaml/steps/bitrise-ssh.test.d.ts +0 -5
- package/dist/src/yaml/steps/bitrise-ssh.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/bitrise-ssh.test.js +0 -205
- package/dist/src/yaml/steps/cache-pull-daemon.test.d.ts +0 -27
- package/dist/src/yaml/steps/cache-pull-daemon.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/cache-pull-daemon.test.js +0 -212
- package/dist/src/yaml/steps/cache-pull-restore-roots.test.d.ts +0 -42
- package/dist/src/yaml/steps/cache-pull-restore-roots.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/cache-pull-restore-roots.test.js +0 -209
- package/dist/src/yaml/steps/cache-push-daemon.test.d.ts +0 -27
- package/dist/src/yaml/steps/cache-push-daemon.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/cache-push-daemon.test.js +0 -171
- package/dist/src/yaml/steps/cache.d.ts +0 -59
- package/dist/src/yaml/steps/cache.d.ts.map +0 -1
- package/dist/src/yaml/steps/cache.js +0 -1027
- package/dist/src/yaml/steps/capture-injection.d.ts +0 -30
- package/dist/src/yaml/steps/capture-injection.d.ts.map +0 -1
- package/dist/src/yaml/steps/capture-injection.js +0 -88
- package/dist/src/yaml/steps/capture-injection.test.d.ts +0 -2
- package/dist/src/yaml/steps/capture-injection.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/capture-injection.test.js +0 -163
- package/dist/src/yaml/steps/cocoapods-deployment-floor.d.ts +0 -42
- package/dist/src/yaml/steps/cocoapods-deployment-floor.d.ts.map +0 -1
- package/dist/src/yaml/steps/cocoapods-deployment-floor.js +0 -140
- package/dist/src/yaml/steps/cocoapods-deployment-floor.test.d.ts +0 -11
- package/dist/src/yaml/steps/cocoapods-deployment-floor.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/cocoapods-deployment-floor.test.js +0 -176
- package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.d.ts +0 -22
- package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.js +0 -97
- package/dist/src/yaml/steps/cocoapods-locked-bundler.test.d.ts +0 -17
- package/dist/src/yaml/steps/cocoapods-locked-bundler.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/cocoapods-locked-bundler.test.js +0 -282
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +0 -17
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +0 -132
- package/dist/src/yaml/steps/envstore-loader.test.d.ts +0 -15
- package/dist/src/yaml/steps/envstore-loader.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/envstore-loader.test.js +0 -143
- package/dist/src/yaml/steps/fastlane.d.ts +0 -27
- package/dist/src/yaml/steps/fastlane.d.ts.map +0 -1
- package/dist/src/yaml/steps/fastlane.js +0 -79
- package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts +0 -53
- package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts.map +0 -1
- package/dist/src/yaml/steps/fidelity-aspect-guard.js +0 -72
- package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts +0 -11
- package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/fidelity-aspect-guard.test.js +0 -50
- package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts +0 -87
- package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts.map +0 -1
- package/dist/src/yaml/steps/fidelity-reference-provenance.js +0 -111
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts +0 -22
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.js +0 -118
- package/dist/src/yaml/steps/file.d.ts +0 -52
- package/dist/src/yaml/steps/file.d.ts.map +0 -1
- package/dist/src/yaml/steps/file.js +0 -108
- package/dist/src/yaml/steps/flutter.d.ts +0 -63
- package/dist/src/yaml/steps/flutter.d.ts.map +0 -1
- package/dist/src/yaml/steps/flutter.js +0 -215
- package/dist/src/yaml/steps/git-clone-names-the-inputs-it-ignores.test.d.ts +0 -16
- package/dist/src/yaml/steps/git-clone-names-the-inputs-it-ignores.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/git-clone-names-the-inputs-it-ignores.test.js +0 -127
- package/dist/src/yaml/steps/git-clone.d.ts +0 -32
- package/dist/src/yaml/steps/git-clone.d.ts.map +0 -1
- package/dist/src/yaml/steps/git-clone.js +0 -171
- package/dist/src/yaml/steps/git-fetch-retry-exec.test.d.ts +0 -10
- package/dist/src/yaml/steps/git-fetch-retry-exec.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/git-fetch-retry-exec.test.js +0 -162
- package/dist/src/yaml/steps/git-fetch-retry.d.ts +0 -62
- package/dist/src/yaml/steps/git-fetch-retry.d.ts.map +0 -1
- package/dist/src/yaml/steps/git-fetch-retry.js +0 -155
- package/dist/src/yaml/steps/git-fetch-retry.test.d.ts +0 -5
- package/dist/src/yaml/steps/git-fetch-retry.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/git-fetch-retry.test.js +0 -86
- package/dist/src/yaml/steps/google-play-deploy.d.ts +0 -37
- package/dist/src/yaml/steps/google-play-deploy.d.ts.map +0 -1
- package/dist/src/yaml/steps/google-play-deploy.js +0 -193
- package/dist/src/yaml/steps/google-play-deploy.test.d.ts +0 -5
- package/dist/src/yaml/steps/google-play-deploy.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/google-play-deploy.test.js +0 -310
- package/dist/src/yaml/steps/gradle-memory.d.ts +0 -61
- package/dist/src/yaml/steps/gradle-memory.d.ts.map +0 -1
- package/dist/src/yaml/steps/gradle-memory.js +0 -386
- package/dist/src/yaml/steps/gradle-memory.test.d.ts +0 -2
- package/dist/src/yaml/steps/gradle-memory.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/gradle-memory.test.js +0 -344
- package/dist/src/yaml/steps/gradle-sdk-cmake.d.ts +0 -28
- package/dist/src/yaml/steps/gradle-sdk-cmake.d.ts.map +0 -1
- package/dist/src/yaml/steps/gradle-sdk-cmake.js +0 -107
- package/dist/src/yaml/steps/gradle-sdk-cmake.test.d.ts +0 -2
- package/dist/src/yaml/steps/gradle-sdk-cmake.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/gradle-sdk-cmake.test.js +0 -183
- package/dist/src/yaml/steps/index.d.ts +0 -10
- package/dist/src/yaml/steps/index.d.ts.map +0 -1
- package/dist/src/yaml/steps/index.js +0 -1461
- package/dist/src/yaml/steps/ios-deps.d.ts +0 -78
- package/dist/src/yaml/steps/ios-deps.d.ts.map +0 -1
- package/dist/src/yaml/steps/ios-deps.js +0 -279
- package/dist/src/yaml/steps/ios-deps.test.d.ts +0 -5
- package/dist/src/yaml/steps/ios-deps.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/ios-deps.test.js +0 -90
- package/dist/src/yaml/steps/ios-signing.d.ts +0 -31
- package/dist/src/yaml/steps/ios-signing.d.ts.map +0 -1
- package/dist/src/yaml/steps/ios-signing.js +0 -144
- package/dist/src/yaml/steps/ios-version.d.ts +0 -47
- package/dist/src/yaml/steps/ios-version.d.ts.map +0 -1
- package/dist/src/yaml/steps/ios-version.js +0 -151
- package/dist/src/yaml/steps/java-home-version-spec.test.d.ts +0 -10
- package/dist/src/yaml/steps/java-home-version-spec.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/java-home-version-spec.test.js +0 -58
- package/dist/src/yaml/steps/linting.d.ts +0 -47
- package/dist/src/yaml/steps/linting.d.ts.map +0 -1
- package/dist/src/yaml/steps/linting.js +0 -148
- package/dist/src/yaml/steps/phase2.test.d.ts +0 -6
- package/dist/src/yaml/steps/phase2.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/phase2.test.js +0 -197
- package/dist/src/yaml/steps/phase3.test.d.ts +0 -5
- package/dist/src/yaml/steps/phase3.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/phase3.test.js +0 -144
- package/dist/src/yaml/steps/phase4.test.d.ts +0 -5
- package/dist/src/yaml/steps/phase4.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/phase4.test.js +0 -166
- package/dist/src/yaml/steps/phase5.test.d.ts +0 -6
- package/dist/src/yaml/steps/phase5.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/phase5.test.js +0 -263
- package/dist/src/yaml/steps/registry.d.ts +0 -88
- package/dist/src/yaml/steps/registry.d.ts.map +0 -1
- package/dist/src/yaml/steps/registry.js +0 -125
- package/dist/src/yaml/steps/registry.test.d.ts +0 -5
- package/dist/src/yaml/steps/registry.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/registry.test.js +0 -235
- package/dist/src/yaml/steps/release.d.ts +0 -50
- package/dist/src/yaml/steps/release.d.ts.map +0 -1
- package/dist/src/yaml/steps/release.js +0 -154
- package/dist/src/yaml/steps/render-post-processor.d.ts +0 -100
- package/dist/src/yaml/steps/render-post-processor.d.ts.map +0 -1
- package/dist/src/yaml/steps/render-post-processor.js +0 -270
- package/dist/src/yaml/steps/render-post-processor.test.d.ts +0 -13
- package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/render-post-processor.test.js +0 -220
- package/dist/src/yaml/steps/script.d.ts +0 -23
- package/dist/src/yaml/steps/script.d.ts.map +0 -1
- package/dist/src/yaml/steps/script.js +0 -63
- package/dist/src/yaml/steps/spec-validation.test.d.ts +0 -6
- package/dist/src/yaml/steps/spec-validation.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/spec-validation.test.js +0 -130
- package/dist/src/yaml/steps/steps.test.d.ts +0 -6
- package/dist/src/yaml/steps/steps.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/steps.test.js +0 -881
- package/dist/src/yaml/steps/structural-facts.d.ts +0 -992
- package/dist/src/yaml/steps/structural-facts.d.ts.map +0 -1
- package/dist/src/yaml/steps/structural-facts.js +0 -3276
- package/dist/src/yaml/steps/structural-facts.test.d.ts +0 -12
- package/dist/src/yaml/steps/structural-facts.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/structural-facts.test.js +0 -3039
- package/dist/src/yaml/steps/test-config.d.ts +0 -5
- package/dist/src/yaml/steps/test-config.d.ts.map +0 -1
- package/dist/src/yaml/steps/test-config.js +0 -25
- package/dist/src/yaml/steps/trim-align-corpus.test.d.ts +0 -28
- package/dist/src/yaml/steps/trim-align-corpus.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/trim-align-corpus.test.js +0 -383
- package/dist/src/yaml/steps/trim-align.d.ts +0 -87
- package/dist/src/yaml/steps/trim-align.d.ts.map +0 -1
- package/dist/src/yaml/steps/trim-align.js +0 -367
- package/dist/src/yaml/steps/trim-align.test.d.ts +0 -11
- package/dist/src/yaml/steps/trim-align.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/trim-align.test.js +0 -261
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +0 -66
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-preview-android.js +0 -251
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts +0 -13
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +0 -302
- package/dist/src/yaml/steps/ui-fidelity-preview.d.ts +0 -134
- package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-preview.js +0 -173
- package/dist/src/yaml/steps/ui-fidelity-preview.test.d.ts +0 -11
- package/dist/src/yaml/steps/ui-fidelity-preview.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-preview.test.js +0 -210
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +0 -155
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +0 -1389
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts +0 -2
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +0 -1106
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +0 -230
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +0 -1597
- package/dist/src/yaml/steps/ui-fidelity-render.test.d.ts +0 -18
- package/dist/src/yaml/steps/ui-fidelity-render.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +0 -1871
- package/dist/src/yaml/steps/xcode-app-product.d.ts +0 -90
- package/dist/src/yaml/steps/xcode-app-product.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-app-product.js +0 -247
- package/dist/src/yaml/steps/xcode-app-product.test.d.ts +0 -2
- package/dist/src/yaml/steps/xcode-app-product.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-app-product.test.js +0 -202
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +0 -30
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-derived-data.test.js +0 -279
- package/dist/src/yaml/steps/xcode-destination.d.ts +0 -47
- package/dist/src/yaml/steps/xcode-destination.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-destination.js +0 -202
- package/dist/src/yaml/steps/xcode-destination.test.d.ts +0 -9
- package/dist/src/yaml/steps/xcode-destination.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-destination.test.js +0 -227
- package/dist/src/yaml/steps/xcode-new.test.d.ts +0 -5
- package/dist/src/yaml/steps/xcode-new.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-new.test.js +0 -211
- package/dist/src/yaml/steps/xcode-resolve-retry.test.d.ts +0 -9
- package/dist/src/yaml/steps/xcode-resolve-retry.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-resolve-retry.test.js +0 -146
- package/dist/src/yaml/steps/xcode-simulator-archs.d.ts +0 -53
- package/dist/src/yaml/steps/xcode-simulator-archs.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-simulator-archs.js +0 -98
- package/dist/src/yaml/steps/xcode-simulator-archs.test.d.ts +0 -21
- package/dist/src/yaml/steps/xcode-simulator-archs.test.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode-simulator-archs.test.js +0 -155
- package/dist/src/yaml/steps/xcode.d.ts +0 -230
- package/dist/src/yaml/steps/xcode.d.ts.map +0 -1
- package/dist/src/yaml/steps/xcode.js +0 -1350
- package/dist/src/yaml/types.d.ts +0 -76
- package/dist/src/yaml/types.d.ts.map +0 -1
- package/dist/src/yaml/types.js +0 -5
- package/dist/src/yaml/validation-types.d.ts +0 -102
- package/dist/src/yaml/validation-types.d.ts.map +0 -1
- package/dist/src/yaml/validation-types.js +0 -8
- package/dist/src/yaml/yaml-updater.d.ts +0 -24
- package/dist/src/yaml/yaml-updater.d.ts.map +0 -1
- package/dist/src/yaml/yaml-updater.js +0 -128
|
@@ -1,1639 +0,0 @@
|
|
|
1
|
-
import { resolve, join, relative, dirname } from "node:path";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
-
import { parseSettingsModules } from "../shared/gradle-settings.js";
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// File discovery
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
/**
|
|
8
|
-
* Returns the list of gradle files to scan.
|
|
9
|
-
* Always includes the root build.gradle and the app/ module.
|
|
10
|
-
* Also parses settings.gradle to find any additional include'd modules.
|
|
11
|
-
*/
|
|
12
|
-
function findGradleFiles(root) {
|
|
13
|
-
const candidates = [];
|
|
14
|
-
// Root-level build files
|
|
15
|
-
for (const name of ["build.gradle", "build.gradle.kts"]) {
|
|
16
|
-
const p = resolve(root, name);
|
|
17
|
-
if (existsSync(p))
|
|
18
|
-
candidates.push(p);
|
|
19
|
-
}
|
|
20
|
-
// Collect module directories from settings.gradle
|
|
21
|
-
const moduleDirs = new Set(["app"]); // always include app/
|
|
22
|
-
for (const name of ["settings.gradle", "settings.gradle.kts"]) {
|
|
23
|
-
const p = resolve(root, name);
|
|
24
|
-
if (!existsSync(p))
|
|
25
|
-
continue;
|
|
26
|
-
const content = readFileSync(p, "utf-8");
|
|
27
|
-
// One parser, shared with the detector. The regex that used to live here
|
|
28
|
-
// matched only the *first* argument of an `include`, so
|
|
29
|
-
// `include(":android", ":common", …)` — one call, eight modules in
|
|
30
|
-
// `SimonSchubert/LinuxCommandLibrary` — was read as one, and the module
|
|
31
|
-
// holding `jvmToolchain(21)` was never opened.
|
|
32
|
-
for (const mod of parseSettingsModules(content))
|
|
33
|
-
moduleDirs.add(mod);
|
|
34
|
-
// An included build is a Gradle project of its own, and a monorepo's
|
|
35
|
-
// convention plugins live in one. Its build file is where the Java level
|
|
36
|
-
// for every module it configures is often declared.
|
|
37
|
-
for (const included of parseIncludedBuilds(content)) {
|
|
38
|
-
for (const buildFile of ["build.gradle", "build.gradle.kts"]) {
|
|
39
|
-
const q = resolve(root, included, buildFile);
|
|
40
|
-
if (existsSync(q))
|
|
41
|
-
candidates.push(q);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
for (const mod of moduleDirs) {
|
|
46
|
-
for (const name of ["build.gradle", "build.gradle.kts"]) {
|
|
47
|
-
const p = resolve(root, mod, name);
|
|
48
|
-
if (existsSync(p))
|
|
49
|
-
candidates.push(p);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return candidates;
|
|
53
|
-
}
|
|
54
|
-
function safeRead(path) {
|
|
55
|
-
try {
|
|
56
|
-
return readFileSync(path, "utf-8");
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return "";
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function relPath(root, filePath) {
|
|
63
|
-
return relative(root, filePath);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Returns line numbers for each match of a regex in file content.
|
|
67
|
-
*/
|
|
68
|
-
function lineNumber(content, index) {
|
|
69
|
-
return content.slice(0, index).split("\n").length;
|
|
70
|
-
}
|
|
71
|
-
/** Extract all System.getenv("VAR") references. */
|
|
72
|
-
function extractEnvVarRefs(content) {
|
|
73
|
-
const results = [];
|
|
74
|
-
const re = /System\.getenv\(\s*["']([^"']+)["']\s*\)/g;
|
|
75
|
-
let m;
|
|
76
|
-
while ((m = re.exec(content)) !== null) {
|
|
77
|
-
results.push({ name: m[1], line: lineNumber(content, m.index) });
|
|
78
|
-
}
|
|
79
|
-
return results;
|
|
80
|
-
}
|
|
81
|
-
/** Extract Groovy findProperty("prop") and Kotlin properties["prop"] references. */
|
|
82
|
-
function extractPropertyRefs(content) {
|
|
83
|
-
const results = [];
|
|
84
|
-
// Groovy: project.findProperty("name") or findProperty("name")
|
|
85
|
-
const groovyRe = /(?:project\.)?findProperty\(\s*["']([^"']+)["']\s*\)/g;
|
|
86
|
-
let m;
|
|
87
|
-
while ((m = groovyRe.exec(content)) !== null) {
|
|
88
|
-
results.push({ name: m[1], line: lineNumber(content, m.index) });
|
|
89
|
-
}
|
|
90
|
-
// Kotlin DSL: properties["name"] or project.properties["name"]
|
|
91
|
-
const kotlinRe = /(?:project\.)?properties\["([^"]+)"\]/g;
|
|
92
|
-
while ((m = kotlinRe.exec(content)) !== null) {
|
|
93
|
-
results.push({ name: m[1], line: lineNumber(content, m.index) });
|
|
94
|
-
}
|
|
95
|
-
return results;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Extract env var names from buildConfigField entries.
|
|
99
|
-
* e.g. buildConfigField "String", "API_KEY", System.getenv("MAPS_API_KEY") ?: ""
|
|
100
|
-
* Returns the System.getenv variable names embedded inside buildConfigField values.
|
|
101
|
-
*/
|
|
102
|
-
function extractBuildConfigEnvRefs(content) {
|
|
103
|
-
const results = [];
|
|
104
|
-
// Match buildConfigField lines that also contain System.getenv
|
|
105
|
-
const lineRe = /buildConfigField\s*[\("]/g;
|
|
106
|
-
let m;
|
|
107
|
-
const envRe = /System\.getenv\(\s*["']([^"']+)["']\s*\)/g;
|
|
108
|
-
while ((m = lineRe.exec(content)) !== null) {
|
|
109
|
-
// Find the end of the statement (next newline or semicolon)
|
|
110
|
-
const lineEnd = content.indexOf("\n", m.index);
|
|
111
|
-
const slice = content.slice(m.index, lineEnd === -1 ? undefined : lineEnd + 1);
|
|
112
|
-
let em;
|
|
113
|
-
while ((em = envRe.exec(slice)) !== null) {
|
|
114
|
-
results.push({ name: em[1], line: lineNumber(content, m.index) });
|
|
115
|
-
}
|
|
116
|
-
envRe.lastIndex = 0;
|
|
117
|
-
}
|
|
118
|
-
return results;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Returns true if the Google Secrets Gradle Plugin is applied.
|
|
122
|
-
* This plugin reads local.properties (or a configured file) and auto-generates
|
|
123
|
-
* BuildConfig fields — so if that file is absent in CI, fields won't be generated.
|
|
124
|
-
*/
|
|
125
|
-
function detectSecretsPlugin(content) {
|
|
126
|
-
return (/alias\s*\(\s*libs\.plugins\.secrets\s*\)/.test(content) ||
|
|
127
|
-
/id\s*\(\s*["']com\.google\.android\.libraries\.mapsplatform\.secrets-gradle-plugin["']\s*\)/.test(content) ||
|
|
128
|
-
/id\s*["']com\.google\.android\.libraries\.mapsplatform\.secrets-gradle-plugin["']/.test(content));
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Extracts the `propertiesFileName` from a `secrets { }` block.
|
|
132
|
-
* Defaults to "local.properties" if not configured.
|
|
133
|
-
*/
|
|
134
|
-
function extractSecretsPropertiesFile(content) {
|
|
135
|
-
const block = extractBlockContent(content, "secrets");
|
|
136
|
-
if (!block)
|
|
137
|
-
return "local.properties";
|
|
138
|
-
const m = /propertiesFileName\s*=\s*["']([^"']+)["']/.exec(block);
|
|
139
|
-
return m ? m[1] : "local.properties";
|
|
140
|
-
}
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
// Property keys the build reads out of the Secrets plugin's properties file
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
/**
|
|
145
|
-
* True when this build file *applies* the Secrets plugin, as opposed to merely
|
|
146
|
-
* declaring its version for other modules.
|
|
147
|
-
*
|
|
148
|
-
* A root build file almost always carries
|
|
149
|
-
* `alias(libs.plugins.secrets) apply false` — the plugin is named there so a
|
|
150
|
-
* module can apply it without repeating the version, and the root project
|
|
151
|
-
* itself never loads a properties file. Reading that as "the root module
|
|
152
|
-
* applies it" would aim the source walk below at the whole repository on the
|
|
153
|
-
* strength of a line that applies nothing.
|
|
154
|
-
*
|
|
155
|
-
* Separate from `detectSecretsPlugin` on purpose: that one decides which
|
|
156
|
-
* warnings are emitted, and this must not change them.
|
|
157
|
-
*/
|
|
158
|
-
function appliesSecretsPlugin(content) {
|
|
159
|
-
const applied = content
|
|
160
|
-
.split("\n")
|
|
161
|
-
.filter((line) => !/\bapply\s+false\b/.test(line))
|
|
162
|
-
.join("\n");
|
|
163
|
-
return detectSecretsPlugin(applied);
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* The `BuildConfig` fields the Android Gradle Plugin generates by itself.
|
|
167
|
-
*
|
|
168
|
-
* `BuildConfig.DEBUG` is the most-referenced constant in Android source, and
|
|
169
|
-
* every one of these is generated whether or not a properties file exists — so
|
|
170
|
-
* naming them costs more than the unused line the rest of this scan tolerates.
|
|
171
|
-
* A property named `DEBUG` would make the Secrets plugin generate a `String
|
|
172
|
-
* DEBUG` beside AGP's `boolean DEBUG`, and the build that was going to compile
|
|
173
|
-
* stops compiling because of the file meant to rescue it.
|
|
174
|
-
*
|
|
175
|
-
* `FLAVOR_<dimension>` is generated once per flavor dimension, so it is matched
|
|
176
|
-
* by prefix rather than listed.
|
|
177
|
-
*/
|
|
178
|
-
const AGP_GENERATED_BUILD_CONFIG_FIELDS = new Set([
|
|
179
|
-
"DEBUG",
|
|
180
|
-
"APPLICATION_ID",
|
|
181
|
-
"BUILD_TYPE",
|
|
182
|
-
"FLAVOR",
|
|
183
|
-
"VERSION_CODE",
|
|
184
|
-
"VERSION_NAME",
|
|
185
|
-
"LIBRARY_PACKAGE_NAME",
|
|
186
|
-
]);
|
|
187
|
-
function isAgpGeneratedField(name) {
|
|
188
|
-
return AGP_GENERATED_BUILD_CONFIG_FIELDS.has(name) || name.startsWith("FLAVOR_");
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* `BuildConfig.SOME_FIELD` references in a source file.
|
|
192
|
-
*
|
|
193
|
-
* Screaming snake case only: that is the convention for a generated constant,
|
|
194
|
-
* and widening it to any identifier would pull in `BuildConfig.javaClass` and
|
|
195
|
-
* every other member access on the class itself.
|
|
196
|
-
*/
|
|
197
|
-
function extractBuildConfigFieldRefs(content) {
|
|
198
|
-
const names = [];
|
|
199
|
-
const re = /\bBuildConfig\.([A-Z][A-Z0-9_]*)\b/g;
|
|
200
|
-
let m;
|
|
201
|
-
while ((m = re.exec(content)) !== null) {
|
|
202
|
-
if (!isAgpGeneratedField(m[1]))
|
|
203
|
-
names.push(m[1]);
|
|
204
|
-
}
|
|
205
|
-
return names;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* The index of the character closing the bracket or parenthesis at `open`,
|
|
209
|
-
* ignoring anything inside a string literal. -1 when it is never closed.
|
|
210
|
-
*/
|
|
211
|
-
function matchingClose(content, open) {
|
|
212
|
-
const closing = content[open] === "[" ? "]" : ")";
|
|
213
|
-
let depth = 0;
|
|
214
|
-
let quote = "";
|
|
215
|
-
for (let i = open; i < content.length; i++) {
|
|
216
|
-
const ch = content[i];
|
|
217
|
-
if (quote) {
|
|
218
|
-
if (ch === "\\")
|
|
219
|
-
i++;
|
|
220
|
-
else if (ch === quote)
|
|
221
|
-
quote = "";
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (ch === '"' || ch === "'")
|
|
225
|
-
quote = ch;
|
|
226
|
-
else if (ch === "[" || ch === "(")
|
|
227
|
-
depth++;
|
|
228
|
-
else if (ch === "]" || ch === ")") {
|
|
229
|
-
depth--;
|
|
230
|
-
if (depth === 0)
|
|
231
|
-
return content[i] === closing ? i : -1;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return -1;
|
|
235
|
-
}
|
|
236
|
-
/** Split a map-literal body on its top-level commas — not ones nested or quoted. */
|
|
237
|
-
function splitTopLevel(body) {
|
|
238
|
-
const parts = [];
|
|
239
|
-
let depth = 0;
|
|
240
|
-
let quote = "";
|
|
241
|
-
let start = 0;
|
|
242
|
-
for (let i = 0; i < body.length; i++) {
|
|
243
|
-
const ch = body[i];
|
|
244
|
-
if (quote) {
|
|
245
|
-
if (ch === "\\")
|
|
246
|
-
i++;
|
|
247
|
-
else if (ch === quote)
|
|
248
|
-
quote = "";
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
if (ch === '"' || ch === "'")
|
|
252
|
-
quote = ch;
|
|
253
|
-
else if (ch === "[" || ch === "(" || ch === "{")
|
|
254
|
-
depth++;
|
|
255
|
-
else if (ch === "]" || ch === ")" || ch === "}")
|
|
256
|
-
depth--;
|
|
257
|
-
else if (ch === "," && depth === 0) {
|
|
258
|
-
parts.push(body.slice(start, i));
|
|
259
|
-
start = i + 1;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
parts.push(body.slice(start));
|
|
263
|
-
return parts;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* The keys of a map literal, in either language's spelling:
|
|
267
|
-
* Groovy `[key: v, "key": v]`, Kotlin `mapOf("key" to v)`.
|
|
268
|
-
*/
|
|
269
|
-
function mapLiteralKeys(body) {
|
|
270
|
-
const names = [];
|
|
271
|
-
for (const entry of splitTopLevel(body)) {
|
|
272
|
-
const kotlin = /^\s*["']([^"']+)["']\s+to\s/.exec(entry);
|
|
273
|
-
if (kotlin) {
|
|
274
|
-
names.push(kotlin[1]);
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
const groovy = /^\s*(?:["']([^"']+)["']|([A-Za-z_]\w*))\s*:/.exec(entry);
|
|
278
|
-
if (groovy)
|
|
279
|
-
names.push(groovy[1] ?? groovy[2]);
|
|
280
|
-
}
|
|
281
|
-
return names;
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Keys named in a `manifestPlaceholders` map in a build file.
|
|
285
|
-
*
|
|
286
|
-
* The Secrets plugin fills a placeholder from the property of the same name,
|
|
287
|
-
* so a placeholder the build declares is a property the build reads. Every
|
|
288
|
-
* spelling the two DSLs offer for the same map:
|
|
289
|
-
*
|
|
290
|
-
* manifestPlaceholders["mapsApiKey"] = … // both
|
|
291
|
-
* manifestPlaceholders.put("mapsApiKey", …) // Groovy
|
|
292
|
-
* manifestPlaceholders = [mapsApiKey: …] // Groovy
|
|
293
|
-
* manifestPlaceholders += mapOf("mapsApiKey" to …) // Kotlin
|
|
294
|
-
*/
|
|
295
|
-
function extractManifestPlaceholderKeys(content) {
|
|
296
|
-
const names = [];
|
|
297
|
-
for (const re of [
|
|
298
|
-
/manifestPlaceholders\s*\[\s*["']([^"']+)["']\s*\]/g,
|
|
299
|
-
/manifestPlaceholders\s*\.\s*put\s*\(\s*["']([^"']+)["']/g,
|
|
300
|
-
]) {
|
|
301
|
-
let m;
|
|
302
|
-
while ((m = re.exec(content)) !== null)
|
|
303
|
-
names.push(m[1]);
|
|
304
|
-
}
|
|
305
|
-
// A whole map assigned, added or put at once. The opening bracket the regex
|
|
306
|
-
// ends on is where the literal starts; its match is where it ends.
|
|
307
|
-
const assigned = /manifestPlaceholders\s*(?:\.\s*putAll\s*\(\s*)?(?:\+?=\s*)?(?:mapOf\s*|mutableMapOf\s*)?[[(]/g;
|
|
308
|
-
let m;
|
|
309
|
-
while ((m = assigned.exec(content)) !== null) {
|
|
310
|
-
const open = m.index + m[0].length - 1;
|
|
311
|
-
const close = matchingClose(content, open);
|
|
312
|
-
if (close === -1)
|
|
313
|
-
continue;
|
|
314
|
-
names.push(...mapLiteralKeys(content.slice(open + 1, close)));
|
|
315
|
-
assigned.lastIndex = close;
|
|
316
|
-
}
|
|
317
|
-
return names;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Placeholder names referenced in an `AndroidManifest.xml`, other than the ones
|
|
321
|
-
* the Android Gradle Plugin injects itself.
|
|
322
|
-
*
|
|
323
|
-
* **This is what the Secrets plugin is mostly for.** Its own documentation, and
|
|
324
|
-
* every Google Maps setup guide, puts the key in the manifest and nowhere else:
|
|
325
|
-
*
|
|
326
|
-
* <meta-data android:name="com.google.android.geo.API_KEY"
|
|
327
|
-
* android:value="${MAPS_API_KEY}" />
|
|
328
|
-
*
|
|
329
|
-
* There is no `BuildConfig` reference and no `manifestPlaceholders` block —
|
|
330
|
-
* the plugin reads `MAPS_API_KEY` from the properties file and substitutes it
|
|
331
|
-
* during manifest merging. A scan that reads only build files and source sees
|
|
332
|
-
* nothing at all in such a repository.
|
|
333
|
-
*
|
|
334
|
-
* `applicationId` is excluded because AGP supplies it from the variant. A name
|
|
335
|
-
* a build file *declares* in `manifestPlaceholders` is not excluded: the build
|
|
336
|
-
* supplies that one, so naming it costs the usual unused line, and telling the
|
|
337
|
-
* two apart would mean resolving which module's manifest merges with which
|
|
338
|
-
* module's build file.
|
|
339
|
-
*/
|
|
340
|
-
const AGP_INJECTED_PLACEHOLDERS = new Set(["applicationId"]);
|
|
341
|
-
function extractManifestXmlPlaceholders(content) {
|
|
342
|
-
const names = [];
|
|
343
|
-
const re = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
344
|
-
let m;
|
|
345
|
-
while ((m = re.exec(content)) !== null) {
|
|
346
|
-
if (!AGP_INJECTED_PLACEHOLDERS.has(m[1]))
|
|
347
|
-
names.push(m[1]);
|
|
348
|
-
}
|
|
349
|
-
return names;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* True when this build file loads the properties file itself, rather than
|
|
353
|
-
* leaving it to the Secrets plugin.
|
|
354
|
-
*
|
|
355
|
-
* Every spelling seen in the wild reduces to naming the file or calling a
|
|
356
|
-
* helper that names it: `rootProject.file("local.properties")`, AGP's own
|
|
357
|
-
* `gradleLocalProperties(rootDir)`, and Compose's `localPropertiesFile`.
|
|
358
|
-
*/
|
|
359
|
-
function readsPropertiesFileDirectly(content) {
|
|
360
|
-
return (/\blocal\.properties\b/.test(content) ||
|
|
361
|
-
/\bgradleLocalProperties\s*\(/.test(content) ||
|
|
362
|
-
/\blocalPropertiesFile\b/.test(content));
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Keys a build file reads out of a properties file it loaded itself.
|
|
366
|
-
*
|
|
367
|
-
* `getProperty("x")` is the direct form, with or without a default. The rest is
|
|
368
|
-
* for the idiom that hides it: a repository defines its own one-line accessor —
|
|
369
|
-
*
|
|
370
|
-
* def getProps(String name) {
|
|
371
|
-
* def f = rootProject.file('local.properties')
|
|
372
|
-
* if (f.exists()) { def p = new Properties(); p.load(...); return p[name] }
|
|
373
|
-
* return ""
|
|
374
|
-
* }
|
|
375
|
-
*
|
|
376
|
-
* — and every key then arrives as `getProps("sign.file")`, under a name chosen
|
|
377
|
-
* by that repository. So a function whose *body* reads the properties file is
|
|
378
|
-
* treated as an accessor for it, and the string literals at its call sites are
|
|
379
|
-
* keys. Without that, a repository like this reads as having none.
|
|
380
|
-
*
|
|
381
|
-
* `properties["x"]` is deliberately not matched: that is the Gradle *project*
|
|
382
|
-
* property channel — `gradle.properties` and `-P` — which `extractPropertyRefs`
|
|
383
|
-
* already reports, and which is not this file.
|
|
384
|
-
*/
|
|
385
|
-
function extractDirectPropertyReads(content) {
|
|
386
|
-
const names = [];
|
|
387
|
-
const direct = /\.\s*getProperty\s*\(\s*["']([^"']+)["']/g;
|
|
388
|
-
let m;
|
|
389
|
-
while ((m = direct.exec(content)) !== null)
|
|
390
|
-
names.push(m[1]);
|
|
391
|
-
for (const accessor of propertiesAccessorNames(content)) {
|
|
392
|
-
const call = new RegExp(`\\b${escapeForRegExp(accessor)}\\s*\\(\\s*["']([^"']+)["']`, "g");
|
|
393
|
-
let c;
|
|
394
|
-
while ((c = call.exec(content)) !== null)
|
|
395
|
-
names.push(c[1]);
|
|
396
|
-
}
|
|
397
|
-
return names;
|
|
398
|
-
}
|
|
399
|
-
function escapeForRegExp(value) {
|
|
400
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
401
|
-
}
|
|
402
|
-
/** Functions declared in this file whose body reads the properties file. */
|
|
403
|
-
function propertiesAccessorNames(content) {
|
|
404
|
-
const names = [];
|
|
405
|
-
// Groovy `def name(...) {` and Kotlin `fun name(...) {`, which is how a
|
|
406
|
-
// build script declares one.
|
|
407
|
-
const decl = /\b(?:def|fun)\s+([A-Za-z_]\w*)\s*\([^)]*\)\s*(?::[^{]*)?\{/g;
|
|
408
|
-
let m;
|
|
409
|
-
while ((m = decl.exec(content)) !== null) {
|
|
410
|
-
// `matchingBrace` counts from inside the block, so hand it the index after
|
|
411
|
-
// the opening brace the declaration regex ended on.
|
|
412
|
-
const body = m.index + m[0].length;
|
|
413
|
-
const close = matchingBrace(content, body);
|
|
414
|
-
if (close === -1)
|
|
415
|
-
continue;
|
|
416
|
-
if (readsPropertiesFileDirectly(content.slice(body, close)))
|
|
417
|
-
names.push(m[1]);
|
|
418
|
-
}
|
|
419
|
-
return names;
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Keys that name a location on the machine running the build, not a value the
|
|
423
|
-
* build needs supplied.
|
|
424
|
-
*
|
|
425
|
-
* Writing one into a generated file puts a developer's own SDK path onto a
|
|
426
|
-
* runner, where it is wrong — and Gradle finds the SDK from `ANDROID_HOME`
|
|
427
|
-
* when the key is absent, which is how every build on a runner resolves it.
|
|
428
|
-
* The Secrets plugin ignores `sdk.dir` for the same reason.
|
|
429
|
-
*/
|
|
430
|
-
const MACHINE_LOCAL_KEYS = new Set(["sdk.dir", "ndk.dir", "cmake.dir"]);
|
|
431
|
-
// Bounds on the source walk, the way `findGradleFiles` bounds itself to the
|
|
432
|
-
// files a build actually declares. A repository is an unbounded tree and this
|
|
433
|
-
// runs on every scan of one.
|
|
434
|
-
const SOURCE_EXTENSIONS = [".kt", ".kts", ".java"];
|
|
435
|
-
const SOURCE_SKIP_DIRS = new Set(["build", "node_modules"]);
|
|
436
|
-
/**
|
|
437
|
-
* **Manifests get their own budget, and a source walk cannot spend it.**
|
|
438
|
-
*
|
|
439
|
-
* They shared one at first, with source walked first, and on a real application
|
|
440
|
-
* that meant the manifests were never reached at all: 1743 source files against
|
|
441
|
-
* a 1500-file cap left nothing, and the keys referenced only as `${KEY}` in a
|
|
442
|
-
* manifest — which is most of them — came back missing. A scan that answers
|
|
443
|
-
* well on a small repository and silently thins out on a large one is the worse
|
|
444
|
-
* of the two failures, because the large one is the one someone is paid to
|
|
445
|
-
* build.
|
|
446
|
-
*
|
|
447
|
-
* There are few manifests and many source files — 47 against 1743 in that same
|
|
448
|
-
* application — so the two are not competing for a scarce resource, they were
|
|
449
|
-
* just sharing the wrong one.
|
|
450
|
-
*/
|
|
451
|
-
const SOURCE_FILE_CAP = 8000;
|
|
452
|
-
const MANIFEST_FILE_CAP = 500;
|
|
453
|
-
const SOURCE_DEPTH_CAP = 12;
|
|
454
|
-
/** Source files under `dir`, up to the shared file budget. */
|
|
455
|
-
function findSourceFiles(dir, budget, depth = 0) {
|
|
456
|
-
if (depth > SOURCE_DEPTH_CAP || budget.left <= 0)
|
|
457
|
-
return [];
|
|
458
|
-
let entries;
|
|
459
|
-
try {
|
|
460
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
463
|
-
return [];
|
|
464
|
-
}
|
|
465
|
-
const files = [];
|
|
466
|
-
const subdirs = [];
|
|
467
|
-
for (const entry of entries) {
|
|
468
|
-
// Dot directories hold caches and VCS state, never source: `.git`,
|
|
469
|
-
// `.gradle`, `.idea`, `.kotlin`.
|
|
470
|
-
if (entry.name.startsWith("."))
|
|
471
|
-
continue;
|
|
472
|
-
if (entry.isDirectory()) {
|
|
473
|
-
if (!SOURCE_SKIP_DIRS.has(entry.name))
|
|
474
|
-
subdirs.push(join(dir, entry.name));
|
|
475
|
-
}
|
|
476
|
-
else if (SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
|
|
477
|
-
if (budget.left <= 0)
|
|
478
|
-
break;
|
|
479
|
-
budget.left--;
|
|
480
|
-
files.push(join(dir, entry.name));
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
for (const sub of subdirs)
|
|
484
|
-
files.push(...findSourceFiles(sub, budget, depth + 1));
|
|
485
|
-
return files;
|
|
486
|
-
}
|
|
487
|
-
/** The directories of `dirs` that no other member of `dirs` contains. */
|
|
488
|
-
function outermost(dirs) {
|
|
489
|
-
return dirs.filter((dir) => !dirs.some((other) => other !== dir && dir.startsWith(other + "/")));
|
|
490
|
-
}
|
|
491
|
-
/** Manifests under `dir`, up to the shared file budget. */
|
|
492
|
-
function findManifests(dir, budget, depth = 0) {
|
|
493
|
-
if (depth > SOURCE_DEPTH_CAP || budget.left <= 0)
|
|
494
|
-
return [];
|
|
495
|
-
let entries;
|
|
496
|
-
try {
|
|
497
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
498
|
-
}
|
|
499
|
-
catch {
|
|
500
|
-
return [];
|
|
501
|
-
}
|
|
502
|
-
const found = [];
|
|
503
|
-
for (const entry of entries) {
|
|
504
|
-
if (entry.name.startsWith("."))
|
|
505
|
-
continue;
|
|
506
|
-
if (entry.isDirectory()) {
|
|
507
|
-
if (!SOURCE_SKIP_DIRS.has(entry.name)) {
|
|
508
|
-
found.push(...findManifests(join(dir, entry.name), budget, depth + 1));
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
else if (entry.name === "AndroidManifest.xml") {
|
|
512
|
-
if (budget.left <= 0)
|
|
513
|
-
break;
|
|
514
|
-
budget.left--;
|
|
515
|
-
found.push(join(dir, entry.name));
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return found;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* The property names the build reads out of the properties file — see
|
|
522
|
-
* `ScanResult.missingPropertyKeys`. Four sources, because a repository may use
|
|
523
|
-
* any of them and most use exactly one:
|
|
524
|
-
*
|
|
525
|
-
* - `${KEY}` in an `AndroidManifest.xml`, which the Secrets plugin fills. The
|
|
526
|
-
* commonest of the four by a distance, and the one its documentation shows.
|
|
527
|
-
* - `manifestPlaceholders` declared in a build file.
|
|
528
|
-
* - `BuildConfig.<FIELD>` in source, for a key read as a generated constant.
|
|
529
|
-
* - `getProperty("key")` in a build file that loads the properties file
|
|
530
|
-
* itself, for a build that does not use the plugin at all.
|
|
531
|
-
*
|
|
532
|
-
* Bounded to the modules that either apply the plugin or read the file
|
|
533
|
-
* directly. A module that does neither cannot be reading a key out of it.
|
|
534
|
-
*/
|
|
535
|
-
function collectPropertyKeys(gradleFiles) {
|
|
536
|
-
// A module with both a `build.gradle` and a `build.gradle.kts` yields its
|
|
537
|
-
// directory twice, and walking it twice would spend the file budget twice.
|
|
538
|
-
const moduleDirs = new Set();
|
|
539
|
-
const keys = new Set();
|
|
540
|
-
for (const filePath of gradleFiles) {
|
|
541
|
-
const content = safeRead(filePath);
|
|
542
|
-
const plugin = appliesSecretsPlugin(content);
|
|
543
|
-
const direct = readsPropertiesFileDirectly(content);
|
|
544
|
-
if (!plugin && !direct)
|
|
545
|
-
continue;
|
|
546
|
-
moduleDirs.add(dirname(filePath));
|
|
547
|
-
for (const key of extractManifestPlaceholderKeys(content))
|
|
548
|
-
keys.add(key);
|
|
549
|
-
if (direct)
|
|
550
|
-
for (const key of extractDirectPropertyReads(content))
|
|
551
|
-
keys.add(key);
|
|
552
|
-
}
|
|
553
|
-
if (moduleDirs.size === 0)
|
|
554
|
-
return [];
|
|
555
|
-
const sourceBudget = { left: SOURCE_FILE_CAP };
|
|
556
|
-
const manifestBudget = { left: MANIFEST_FILE_CAP };
|
|
557
|
-
for (const dir of outermost([...moduleDirs])) {
|
|
558
|
-
// Manifests first. They are the likeliest place for a key and the cheapest
|
|
559
|
-
// to read, so if anything is ever going to run short it must not be these.
|
|
560
|
-
for (const manifest of findManifests(dir, manifestBudget)) {
|
|
561
|
-
for (const name of extractManifestXmlPlaceholders(safeRead(manifest)))
|
|
562
|
-
keys.add(name);
|
|
563
|
-
}
|
|
564
|
-
for (const file of findSourceFiles(dir, sourceBudget)) {
|
|
565
|
-
for (const name of extractBuildConfigFieldRefs(safeRead(file)))
|
|
566
|
-
keys.add(name);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
for (const key of MACHINE_LOCAL_KEYS)
|
|
570
|
-
keys.delete(key);
|
|
571
|
-
return [...keys].sort();
|
|
572
|
-
}
|
|
573
|
-
/** Parse property keys from a standard .properties file, skipping comments and blank lines. */
|
|
574
|
-
function parsePropertyKeys(propertiesContent) {
|
|
575
|
-
return propertiesContent
|
|
576
|
-
.split("\n")
|
|
577
|
-
.map((l) => l.trim())
|
|
578
|
-
.filter((l) => l && !l.startsWith("#") && l.includes("="))
|
|
579
|
-
.map((l) => l.split("=")[0].trim())
|
|
580
|
-
.filter(Boolean);
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* Removes `if (file("…").exists()) { … }` blocks from build-file content.
|
|
584
|
-
*
|
|
585
|
-
* A plugin applied inside one of these is *conditional on a file the build
|
|
586
|
-
* may not have*. Projects use the idiom precisely so a checkout without the
|
|
587
|
-
* credential still builds:
|
|
588
|
-
*
|
|
589
|
-
* if (file("google-services.json").exists()) {
|
|
590
|
-
* apply(plugin = libs.plugins.gms.googleServices.get().pluginId)
|
|
591
|
-
* }
|
|
592
|
-
*
|
|
593
|
-
* Reading the declaration and ignoring the guard turns "this project builds
|
|
594
|
-
* fine without a credential" into "this project cannot build without one" —
|
|
595
|
-
* a warning that sends the user looking for a file the build never asks for.
|
|
596
|
-
*
|
|
597
|
-
* Brace matching skips quoted strings so a `{` inside a literal cannot end
|
|
598
|
-
* the block early. Anything it cannot match to a close brace is left alone:
|
|
599
|
-
* losing a real declaration is worse than keeping a guarded one.
|
|
600
|
-
*/
|
|
601
|
-
export function stripExistenceGuardedBlocks(content) {
|
|
602
|
-
const guard = /if\s*\(\s*[A-Za-z0-9_.]*file\s*\(\s*[^)]*\)\s*\.exists\s*\(\s*\)\s*\)\s*\{/g;
|
|
603
|
-
let out = "";
|
|
604
|
-
let cursor = 0;
|
|
605
|
-
let match;
|
|
606
|
-
while ((match = guard.exec(content)) !== null) {
|
|
607
|
-
const bodyStart = match.index + match[0].length;
|
|
608
|
-
const end = matchingBrace(content, bodyStart);
|
|
609
|
-
if (end === -1)
|
|
610
|
-
continue;
|
|
611
|
-
out += content.slice(cursor, match.index);
|
|
612
|
-
cursor = end + 1;
|
|
613
|
-
guard.lastIndex = cursor;
|
|
614
|
-
}
|
|
615
|
-
return out + content.slice(cursor);
|
|
616
|
-
}
|
|
617
|
-
/** Index of the `}` closing a block whose body starts at `start`, or -1. */
|
|
618
|
-
function matchingBrace(content, start) {
|
|
619
|
-
let depth = 1;
|
|
620
|
-
let quote = null;
|
|
621
|
-
for (let i = start; i < content.length; i++) {
|
|
622
|
-
const ch = content[i];
|
|
623
|
-
if (quote) {
|
|
624
|
-
if (ch === "\\")
|
|
625
|
-
i++;
|
|
626
|
-
else if (ch === quote)
|
|
627
|
-
quote = null;
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
if (ch === '"' || ch === "'")
|
|
631
|
-
quote = ch;
|
|
632
|
-
else if (ch === "{")
|
|
633
|
-
depth++;
|
|
634
|
-
else if (ch === "}" && --depth === 0)
|
|
635
|
-
return i;
|
|
636
|
-
}
|
|
637
|
-
return -1;
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Returns true if the GMS google-services plugin is applied unconditionally
|
|
641
|
-
* in this content. A declaration guarded by the presence of
|
|
642
|
-
* `google-services.json` is not a credential requirement — see
|
|
643
|
-
* `stripExistenceGuardedBlocks`.
|
|
644
|
-
*/
|
|
645
|
-
function detectGmsPlugin(content) {
|
|
646
|
-
const unguarded = stripExistenceGuardedBlocks(content);
|
|
647
|
-
return (/apply\s+plugin\s*:\s*['"]com\.google\.gms\.google-services['"]/.test(unguarded) ||
|
|
648
|
-
/id\s*\(\s*["']com\.google\.gms\.google-services["']\s*\)/.test(unguarded) ||
|
|
649
|
-
/id\s*["']com\.google\.gms\.google-services["']/.test(unguarded));
|
|
650
|
-
}
|
|
651
|
-
/** Returns true if a signingConfigs block exists. */
|
|
652
|
-
function detectSigningConfigs(content) {
|
|
653
|
-
return /signingConfigs\s*\{/.test(content);
|
|
654
|
-
}
|
|
655
|
-
/**
|
|
656
|
-
* Extract env var and property references from within signingConfigs blocks.
|
|
657
|
-
* Looks for storeFile, storePassword, keyAlias, keyPassword assignments.
|
|
658
|
-
*/
|
|
659
|
-
function parseSigningCredentialRefs(content) {
|
|
660
|
-
const results = [];
|
|
661
|
-
const blockRe = /signingConfigs\s*\{([\s\S]*?)^\s*\}/m;
|
|
662
|
-
const match = blockRe.exec(content);
|
|
663
|
-
if (!match)
|
|
664
|
-
return results;
|
|
665
|
-
const block = match[1];
|
|
666
|
-
const blockStart = match.index;
|
|
667
|
-
// System.getenv inside signing block
|
|
668
|
-
const envRe = /System\.getenv\(\s*["']([^"']+)["']\s*\)/g;
|
|
669
|
-
let m;
|
|
670
|
-
while ((m = envRe.exec(block)) !== null) {
|
|
671
|
-
results.push({ name: m[1], line: lineNumber(content, blockStart + m.index) });
|
|
672
|
-
}
|
|
673
|
-
// findProperty / properties[] inside signing block
|
|
674
|
-
const propRe = /(?:project\.)?findProperty\(\s*["']([^"']+)["']\s*\)|(?:project\.)?properties\["([^"]+)"\]/g;
|
|
675
|
-
while ((m = propRe.exec(block)) !== null) {
|
|
676
|
-
results.push({ name: m[1] ?? m[2], line: lineNumber(content, blockStart + m.index) });
|
|
677
|
-
}
|
|
678
|
-
return results;
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Extract referenced keystore file paths from signingConfigs.
|
|
682
|
-
* Handles: storeFile file("path") and storeFile = file("path")
|
|
683
|
-
*/
|
|
684
|
-
function extractKeystorePaths(content) {
|
|
685
|
-
const paths = [];
|
|
686
|
-
const re = /storeFile\s*(?:=\s*)?file\(\s*["']([^"']+)["']\s*\)/g;
|
|
687
|
-
let m;
|
|
688
|
-
while ((m = re.exec(content)) !== null) {
|
|
689
|
-
paths.push(m[1]);
|
|
690
|
-
}
|
|
691
|
-
return paths;
|
|
692
|
-
}
|
|
693
|
-
// ---------------------------------------------------------------------------
|
|
694
|
-
// Java version detection
|
|
695
|
-
// ---------------------------------------------------------------------------
|
|
696
|
-
/**
|
|
697
|
-
* Parses a raw version token into a major Java version number.
|
|
698
|
-
* Handles formats: "21", "17", "1.8" (→ 8), "VERSION_21", "VERSION_1_8" (→ 8).
|
|
699
|
-
*/
|
|
700
|
-
function parseJavaVersion(raw) {
|
|
701
|
-
// `JvmTarget.fromTarget("17")` names the version inside a string.
|
|
702
|
-
const fromTarget = /fromTarget\s*\(\s*["']([^"']+)["']/.exec(raw);
|
|
703
|
-
if (fromTarget)
|
|
704
|
-
return parseJavaVersion(fromTarget[1]);
|
|
705
|
-
// JavaVersion enum: VERSION_21, VERSION_17, VERSION_1_8
|
|
706
|
-
// JvmTarget enum: JVM_21, JVM_17, JVM_1_8
|
|
707
|
-
const enumMatch = /(?:VERSION|JVM)_1_(\d)$/.exec(raw);
|
|
708
|
-
if (enumMatch)
|
|
709
|
-
return parseInt(enumMatch[1], 10);
|
|
710
|
-
const enumMatch2 = /(?:VERSION|JVM)_(\d+)$/.exec(raw);
|
|
711
|
-
if (enumMatch2)
|
|
712
|
-
return parseInt(enumMatch2[1], 10);
|
|
713
|
-
// String literal: "21", "17", "11", "1.8"
|
|
714
|
-
const legacyMatch = /^1\.(\d)$/.exec(raw.trim());
|
|
715
|
-
if (legacyMatch)
|
|
716
|
-
return parseInt(legacyMatch[1], 10);
|
|
717
|
-
const plain = parseInt(raw.trim(), 10);
|
|
718
|
-
if (!isNaN(plain))
|
|
719
|
-
return plain;
|
|
720
|
-
return undefined;
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Scans Gradle file content for Java / JVM version requirements and returns
|
|
724
|
-
* the highest version number found.
|
|
725
|
-
*
|
|
726
|
-
* Detects:
|
|
727
|
-
* - sourceCompatibility / targetCompatibility (Groovy & Kotlin DSL)
|
|
728
|
-
* - kotlinOptions { jvmTarget = "21" }
|
|
729
|
-
* - jvmToolchain(21) / jvmToolchain { languageVersion.set(JavaLanguageVersion.of(21)) }
|
|
730
|
-
* - java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }
|
|
731
|
-
*/
|
|
732
|
-
function detectJavaVersionInContent(content) {
|
|
733
|
-
const candidates = [];
|
|
734
|
-
// sourceCompatibility / targetCompatibility = JavaVersion.VERSION_21 or = "21" or = JavaVersion.VERSION_1_8
|
|
735
|
-
const compatRe = /(?:sourceCompatibility|targetCompatibility)\s*[=\s]+([A-Za-z0-9._]+)/g;
|
|
736
|
-
let m;
|
|
737
|
-
while ((m = compatRe.exec(content)) !== null) {
|
|
738
|
-
const v = parseJavaVersion(m[1]);
|
|
739
|
-
if (v)
|
|
740
|
-
candidates.push(v);
|
|
741
|
-
}
|
|
742
|
-
// jvmTarget, in every form a Kotlin build writes it:
|
|
743
|
-
// jvmTarget = "21" (kotlinOptions, Groovy)
|
|
744
|
-
// jvmTarget.set(JvmTarget.JVM_17) (compilerOptions, the form
|
|
745
|
-
// LinuxCommandLibrary uses
|
|
746
|
-
// everywhere and we read in
|
|
747
|
-
// none of them)
|
|
748
|
-
// jvmTarget = JvmTarget.JVM_21
|
|
749
|
-
// jvmTarget.set(JvmTarget.fromTarget("17"))
|
|
750
|
-
const jvmTargetFromTargetRe = /jvmTarget\s*(?:=|\.set\s*\(\s*)\s*JvmTarget\.fromTarget\s*\(\s*["']([^"']+)["']/g;
|
|
751
|
-
while ((m = jvmTargetFromTargetRe.exec(content)) !== null) {
|
|
752
|
-
const v = parseJavaVersion(m[1]);
|
|
753
|
-
if (v)
|
|
754
|
-
candidates.push(v);
|
|
755
|
-
}
|
|
756
|
-
const jvmTargetRe = /jvmTarget\s*(?:=|\.set\s*\(\s*)\s*(?:JvmTarget\.)?["']?([A-Za-z0-9._]+)["']?/g;
|
|
757
|
-
while ((m = jvmTargetRe.exec(content)) !== null) {
|
|
758
|
-
const v = parseJavaVersion(m[1]);
|
|
759
|
-
if (v)
|
|
760
|
-
candidates.push(v);
|
|
761
|
-
}
|
|
762
|
-
// jvmToolchain(21)
|
|
763
|
-
const toolchainRe = /jvmToolchain\s*\(\s*(\d+)\s*\)/g;
|
|
764
|
-
while ((m = toolchainRe.exec(content)) !== null) {
|
|
765
|
-
candidates.push(parseInt(m[1], 10));
|
|
766
|
-
}
|
|
767
|
-
// JavaLanguageVersion.of(21)
|
|
768
|
-
const langVerRe = /JavaLanguageVersion\.of\s*\(\s*(\d+)\s*\)/g;
|
|
769
|
-
while ((m = langVerRe.exec(content)) !== null) {
|
|
770
|
-
candidates.push(parseInt(m[1], 10));
|
|
771
|
-
}
|
|
772
|
-
return candidates.length > 0 ? Math.max(...candidates) : undefined;
|
|
773
|
-
}
|
|
774
|
-
/** The builds a settings file composes in, e.g. `includeBuild("build-logic")`. */
|
|
775
|
-
export function parseIncludedBuilds(content) {
|
|
776
|
-
if (!content || typeof content !== "string")
|
|
777
|
-
return [];
|
|
778
|
-
const builds = [];
|
|
779
|
-
for (const m of content.matchAll(/includeBuild\s*\(?\s*["']([^"']+)["']/g)) {
|
|
780
|
-
if (!builds.includes(m[1]))
|
|
781
|
-
builds.push(m[1]);
|
|
782
|
-
}
|
|
783
|
-
return builds;
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* A Java version pinned outside Gradle, and the file that pins it.
|
|
787
|
-
*
|
|
788
|
-
* `mihonapp/mihon` declares no Java level in any Gradle file and pins 21 in
|
|
789
|
-
* `.github/.java-version`, which its own workflows consume through
|
|
790
|
-
* `setup-java`'s `java-version-file:`. The project says what it needs; it
|
|
791
|
-
* just does not say it in Gradle, and reading only Gradle files scanned it as
|
|
792
|
-
* "nothing declared" and ran it on the default. Across the thirty clones,
|
|
793
|
-
* three repositories pin with a version file and ten name a version in a
|
|
794
|
-
* workflow.
|
|
795
|
-
*
|
|
796
|
-
* A pin is a **floor**, folded in with `Math.max` like every other
|
|
797
|
-
* declaration — not an override.
|
|
798
|
-
*/
|
|
799
|
-
export function detectPinnedJavaVersion(root) {
|
|
800
|
-
const found = [];
|
|
801
|
-
const offer = (version, source) => {
|
|
802
|
-
if (version !== undefined && version > 0)
|
|
803
|
-
found.push({ version, source });
|
|
804
|
-
};
|
|
805
|
-
for (const name of [".java-version", ".github/.java-version"]) {
|
|
806
|
-
const content = safeRead(resolve(root, name));
|
|
807
|
-
if (content)
|
|
808
|
-
offer(parseJavaVersion(content.trim().split("\n")[0] ?? ""), name);
|
|
809
|
-
}
|
|
810
|
-
// asdf / mise: `java temurin-21.0.3+9` — the major version is what matters.
|
|
811
|
-
const toolVersions = safeRead(resolve(root, ".tool-versions"));
|
|
812
|
-
if (toolVersions) {
|
|
813
|
-
const m = /^\s*java\s+(\S+)/m.exec(toolVersions);
|
|
814
|
-
if (m)
|
|
815
|
-
offer(parseJavaVersion(m[1].replace(/^[A-Za-z-]+-/, "")), ".tool-versions");
|
|
816
|
-
}
|
|
817
|
-
// SDKMAN: `java=17.0.9-tem`
|
|
818
|
-
const sdkmanrc = safeRead(resolve(root, ".sdkmanrc"));
|
|
819
|
-
if (sdkmanrc) {
|
|
820
|
-
const m = /^\s*java\s*=\s*(\S+)/m.exec(sdkmanrc);
|
|
821
|
-
if (m)
|
|
822
|
-
offer(parseJavaVersion(m[1]), ".sdkmanrc");
|
|
823
|
-
}
|
|
824
|
-
for (const workflow of listWorkflowFiles(root)) {
|
|
825
|
-
const content = safeRead(workflow);
|
|
826
|
-
if (!content)
|
|
827
|
-
continue;
|
|
828
|
-
const name = relative(root, workflow);
|
|
829
|
-
// `java-version: 21`, `java-version: '21'`, and a matrix list — take the
|
|
830
|
-
// highest, because that is the one that has to work.
|
|
831
|
-
for (const m of content.matchAll(/java-version\s*:\s*(.+)/g)) {
|
|
832
|
-
for (const token of m[1].split(/[,[\]\s]+/)) {
|
|
833
|
-
const cleaned = token.replace(/["']/g, "").trim();
|
|
834
|
-
if (cleaned)
|
|
835
|
-
offer(parseJavaVersion(cleaned), name);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
// `java-version-file: .github/.java-version` — read what it points at.
|
|
839
|
-
for (const m of content.matchAll(/java-version-file\s*:\s*["']?([^"'\s]+)/g)) {
|
|
840
|
-
const pinned = safeRead(resolve(root, m[1]));
|
|
841
|
-
if (pinned)
|
|
842
|
-
offer(parseJavaVersion(pinned.trim().split("\n")[0] ?? ""), m[1]);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
if (found.length === 0)
|
|
846
|
-
return undefined;
|
|
847
|
-
return found.reduce((a, b) => (b.version > a.version ? b : a));
|
|
848
|
-
}
|
|
849
|
-
function listWorkflowFiles(root) {
|
|
850
|
-
const dir = resolve(root, ".github", "workflows");
|
|
851
|
-
try {
|
|
852
|
-
return readdirSync(dir)
|
|
853
|
-
.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"))
|
|
854
|
-
.map((f) => resolve(dir, f));
|
|
855
|
-
}
|
|
856
|
-
catch {
|
|
857
|
-
return [];
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Returns the highest Java version requirement found across all Gradle files,
|
|
862
|
-
* or undefined if none is detected.
|
|
863
|
-
*/
|
|
864
|
-
function detectRequiredJavaVersion(gradleFiles) {
|
|
865
|
-
const versions = [];
|
|
866
|
-
for (const filePath of gradleFiles) {
|
|
867
|
-
const content = safeRead(filePath);
|
|
868
|
-
const v = detectJavaVersionInContent(content);
|
|
869
|
-
if (v !== undefined)
|
|
870
|
-
versions.push(v);
|
|
871
|
-
}
|
|
872
|
-
return versions.length > 0 ? Math.max(...versions) : undefined;
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* The Gradle version a wrapper pins, read out of its `distributionUrl`.
|
|
876
|
-
* `…/gradle-8.9-bin.zip` → `8.9`.
|
|
877
|
-
*
|
|
878
|
-
* This bounds which JDK the project can build on: Gradle refuses to start on
|
|
879
|
-
* a JDK newer than it knows about, so the wrapper is a hard ceiling that no
|
|
880
|
-
* `sourceCompatibility` declaration can raise.
|
|
881
|
-
*
|
|
882
|
-
* The trailing `-bin` / `-all` belongs to the file name rather than the
|
|
883
|
-
* version, and a release candidate spends two more hyphens on `-rc-1`, so the
|
|
884
|
-
* version group has to be able to give its last segment back.
|
|
885
|
-
*/
|
|
886
|
-
export function parseGradleWrapperVersion(content) {
|
|
887
|
-
const match = /gradle-(\d+(?:\.\d+)*(?:-[A-Za-z0-9.]+)*)-(?:bin|all)\.zip/.exec(content);
|
|
888
|
-
return match ? match[1] : undefined;
|
|
889
|
-
}
|
|
890
|
-
function detectGradleWrapperVersion(projectRoot) {
|
|
891
|
-
const path = resolve(projectRoot, "gradle", "wrapper", "gradle-wrapper.properties");
|
|
892
|
-
if (!existsSync(path))
|
|
893
|
-
return undefined;
|
|
894
|
-
return parseGradleWrapperVersion(safeRead(path));
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* The newest Java release a given Gradle version can run on, or undefined
|
|
898
|
-
* when the version string says nothing usable.
|
|
899
|
-
*/
|
|
900
|
-
export function gradleJavaCeiling(gradleVersion) {
|
|
901
|
-
const parsed = /^(\d+)(?:\.(\d+))?/.exec(gradleVersion ?? "");
|
|
902
|
-
const major = parsed ? Number(parsed[1]) : NaN;
|
|
903
|
-
if (!Number.isInteger(major))
|
|
904
|
-
return undefined;
|
|
905
|
-
const minor = Number(parsed?.[2] ?? 0);
|
|
906
|
-
const atLeast = (m, n) => major > m || (major === m && minor >= n);
|
|
907
|
-
// Minor-aware, because the boundaries are minor releases. While 17 was the
|
|
908
|
-
// preferred JDK this was harmless — the ceiling only had to be right about
|
|
909
|
-
// whether 17 fitted. Preferring the newest makes it load-bearing: JDK 21
|
|
910
|
-
// arrived in Gradle 8.5, so 8.0 through 8.4 answering "21" would hand those
|
|
911
|
-
// projects a JDK their own wrapper refuses to start on.
|
|
912
|
-
// Gradle 9.1 added JDK 25; 9.0 tops out at 24. Without this the six corpus
|
|
913
|
-
// repositories declaring a level above 21 would still abstain, because a
|
|
914
|
-
// floor of 25 against a ceiling of 24 intersects with nothing — installing
|
|
915
|
-
// JDK 25 buys nothing if no ceiling ever reaches it.
|
|
916
|
-
if (atLeast(9, 1))
|
|
917
|
-
return 25;
|
|
918
|
-
if (major >= 9)
|
|
919
|
-
return 24;
|
|
920
|
-
if (atLeast(8, 5))
|
|
921
|
-
return 21;
|
|
922
|
-
if (atLeast(7, 3))
|
|
923
|
-
return 17;
|
|
924
|
-
if (atLeast(5, 0))
|
|
925
|
-
return 11;
|
|
926
|
-
return 8;
|
|
927
|
-
}
|
|
928
|
-
/** The oldest Java release a given Gradle version will start on. */
|
|
929
|
-
export function gradleJavaFloor(gradleVersion) {
|
|
930
|
-
const major = Number(/^(\d+)\./.exec(gradleVersion ?? "")?.[1]);
|
|
931
|
-
if (!Number.isInteger(major))
|
|
932
|
-
return undefined;
|
|
933
|
-
return major >= 9 ? 17 : 8;
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* The JDKs a build machine installs, and therefore the only values the
|
|
937
|
-
* generated `set-java-version` step can satisfy.
|
|
938
|
-
*
|
|
939
|
-
* 11 joined the images on 2026-09-05, for Gradle 5.x and 6.x — which cannot
|
|
940
|
-
* *start* on 17, whatever the sources declare. It is a fallback, never a
|
|
941
|
-
* preference: the selection takes the newest JDK that fits, so 11 is reached
|
|
942
|
-
* only when a wrapper's ceiling leaves nothing else.
|
|
943
|
-
*
|
|
944
|
-
* 8 and 25 joined on 2026-09-07, the companion half of the "newest the
|
|
945
|
-
* wrapper allows" decision — the images carry every JDK we find a project
|
|
946
|
-
* asking for, rather than a project bending to what they carry. 8 is 11's
|
|
947
|
-
* argument one line further back (Gradle 4.x tops out at Java 8); 25 covers
|
|
948
|
-
* the six repositories in the 150-repo corpus that declare a level above 21.
|
|
949
|
-
* Nothing reaches 8 unless a wrapper ceiling forces it.
|
|
950
|
-
*/
|
|
951
|
-
export const AVAILABLE_JAVA_VERSIONS = [8, 11, 17, 21, 25];
|
|
952
|
-
/**
|
|
953
|
-
* The newest JDK a build machine carries.
|
|
954
|
-
*
|
|
955
|
-
* It is no longer what the chooser falls back to. It used to be: with no JDK
|
|
956
|
-
* satisfying both the sources and the wrapper, `runnableJavaVersion` returned
|
|
957
|
-
* this — which on a project pinning a modern Java level under an old Gradle
|
|
958
|
-
* wrapper is a JDK **above** that wrapper's ceiling, and the build dies at
|
|
959
|
-
* `:app:processDebugMainManifest` with "module java.base does not opens
|
|
960
|
-
* java.io to unnamed module". A number above the ceiling is not a fallback;
|
|
961
|
-
* it is the failure, chosen deliberately and reported as success.
|
|
962
|
-
*/
|
|
963
|
-
export const NEWEST_AVAILABLE_JAVA_VERSION = Math.max(...AVAILABLE_JAVA_VERSIONS);
|
|
964
|
-
/**
|
|
965
|
-
* The JDK to build on, with the reason attached — one place where the
|
|
966
|
-
* constraints are resolved, so the answer and the explanation cannot disagree.
|
|
967
|
-
*
|
|
968
|
-
* They used to. The chooser fell back to the newest installed JDK whenever
|
|
969
|
-
* nothing satisfied both constraints, and the sentence beside it had two
|
|
970
|
-
* branches — a ceiling below every installed JDK, and a declaration above
|
|
971
|
-
* every installed JDK — so the commonest shape of all fell through both and
|
|
972
|
-
* said nothing at all. A project pinning Java 12 against a Gradle wrapper that
|
|
973
|
-
* runs on 11 at most got Java 25 and silence.
|
|
974
|
-
*
|
|
975
|
-
* Three facts decide it:
|
|
976
|
-
*
|
|
977
|
-
* - **What the build machine has.** Only `AVAILABLE_JAVA_VERSIONS` exist, so
|
|
978
|
-
* a project declaring 30 cannot simply be given 30.
|
|
979
|
-
* - **What Gradle can run on.** The wrapper sets a ceiling that source
|
|
980
|
-
* compatibility cannot raise: Gradle starts before it reads a single
|
|
981
|
-
* `sourceCompatibility`.
|
|
982
|
-
* - **What the sources ask for.** A declaration is a floor, not an exact
|
|
983
|
-
* request — a project declaring 11 compiles fine on 21.
|
|
984
|
-
*
|
|
985
|
-
* `kind` distinguishes the five outcomes:
|
|
986
|
-
*
|
|
987
|
-
* - `unknown` — nothing declared and no readable wrapper. Nothing to go on
|
|
988
|
-
* and nothing to say.
|
|
989
|
-
* - `satisfies` — an installed JDK meets both the floor and the ceiling.
|
|
990
|
-
* - `capped-by-wrapper` — nothing meets both, and the wrapper is why. The
|
|
991
|
-
* ceiling is hard and the declaration is a preference, so the newest JDK
|
|
992
|
-
* the wrapper *can* run is the answer, and the caller says so.
|
|
993
|
-
* - `above-runners` — the declaration is above every installed JDK, and that
|
|
994
|
-
* rather than the wrapper is what binds. **No answer**: asking for it would
|
|
995
|
-
* fail when the JDK is selected, and quietly picking a lower one would be
|
|
996
|
-
* the guess this exists to avoid.
|
|
997
|
-
* - `wrapper-below-runners` — the ceiling is below every installed JDK. No
|
|
998
|
-
* answer exists.
|
|
999
|
-
*
|
|
1000
|
-
* `above-runners` is checked before `capped-by-wrapper` so a project declaring
|
|
1001
|
-
* 99 against a modern wrapper is told about the machines rather than about its
|
|
1002
|
-
* wrapper: both bind, and the one it can act on is the one worth naming.
|
|
1003
|
-
*/
|
|
1004
|
-
export function chooseJavaVersion(detectedJavaVersion, gradleVersion) {
|
|
1005
|
-
const declared = Number(detectedJavaVersion) || 0;
|
|
1006
|
-
const ceiling = gradleJavaCeiling(gradleVersion);
|
|
1007
|
-
const floor = gradleJavaFloor(gradleVersion);
|
|
1008
|
-
const installed = [...AVAILABLE_JAVA_VERSIONS].sort((a, b) => a - b);
|
|
1009
|
-
const base = { declared, ceiling, installed };
|
|
1010
|
-
if (!declared && floor === undefined) {
|
|
1011
|
-
return { ...base, version: undefined, kind: "unknown" };
|
|
1012
|
-
}
|
|
1013
|
-
const wanted = Math.max(declared, floor ?? 0);
|
|
1014
|
-
const usable = installed.filter((v) => v >= wanted && v <= (ceiling ?? Infinity));
|
|
1015
|
-
// The newest that fits. Nothing reaches the oldest installed JDK unless a
|
|
1016
|
-
// ceiling leaves nothing else, which is the only reason 8 and 11 are on the
|
|
1017
|
-
// images at all.
|
|
1018
|
-
if (usable.length > 0) {
|
|
1019
|
-
return { ...base, version: usable[usable.length - 1], kind: "satisfies" };
|
|
1020
|
-
}
|
|
1021
|
-
if (declared > installed[installed.length - 1]) {
|
|
1022
|
-
return { ...base, version: undefined, kind: "above-runners" };
|
|
1023
|
-
}
|
|
1024
|
-
const underCeiling = installed.filter((v) => v <= (ceiling ?? Infinity));
|
|
1025
|
-
if (underCeiling.length > 0) {
|
|
1026
|
-
return {
|
|
1027
|
-
...base,
|
|
1028
|
-
version: underCeiling[underCeiling.length - 1],
|
|
1029
|
-
kind: "capped-by-wrapper",
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
return { ...base, version: undefined, kind: "wrapper-below-runners" };
|
|
1033
|
-
}
|
|
1034
|
-
/**
|
|
1035
|
-
* The Java version to write into a generated pipeline: the **newest**
|
|
1036
|
-
* available JDK that satisfies both what the sources declare and what the
|
|
1037
|
-
* Gradle wrapper can run on.
|
|
1038
|
-
*
|
|
1039
|
-
* DECIDED 2026-09-06 (Alex), reversing Invarn PRD decision #15 ("17 is the
|
|
1040
|
-
* JDK whenever nothing forces otherwise"). The thirty-app study measured the
|
|
1041
|
-
* cost of preferring 17 in one direction and none in the other: two rows
|
|
1042
|
-
* failed because 17 was too *low* for a build plugin's own runtime
|
|
1043
|
-
* requirement, and no row failed because 21 was too high. A project targeting
|
|
1044
|
-
* 17 runs fine on 21; never the reverse.
|
|
1045
|
-
*
|
|
1046
|
-
* Source compatibility is a floor, not an exact request. Gradle starts before
|
|
1047
|
-
* it reads a single `sourceCompatibility`, so a wrapper pinning an old Gradle
|
|
1048
|
-
* caps the JDK no matter what the sources say — which is why the ceiling
|
|
1049
|
-
* above had to learn minor versions before this could ship.
|
|
1050
|
-
*/
|
|
1051
|
-
export function runnableJavaVersion(detectedJavaVersion, gradleVersion) {
|
|
1052
|
-
return chooseJavaVersion(detectedJavaVersion, gradleVersion).version;
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* What the choice could not honour, as a sentence, or undefined when it
|
|
1056
|
-
* honoured everything.
|
|
1057
|
-
*
|
|
1058
|
-
* Silence is the failure this exists to prevent: a pipeline that quietly
|
|
1059
|
-
* overruled what a project asked for reads as the project's own problem, and
|
|
1060
|
-
* the commonest shape of all — an old wrapper under a modern declaration — was
|
|
1061
|
-
* the one that used to fall through every branch.
|
|
1062
|
-
*
|
|
1063
|
-
* `source` names the file that asked, when the caller knows it. The scan now
|
|
1064
|
-
* returns it, so "asks for Java 12" can become "asks for Java 12
|
|
1065
|
-
* (.github/workflows/Build.yml)" — a version with no provenance sends a reader
|
|
1066
|
-
* looking through every Gradle file for a number that is not in any of them.
|
|
1067
|
-
*/
|
|
1068
|
-
export function javaVersionProblem(detectedJavaVersion, gradleVersion, source) {
|
|
1069
|
-
const choice = chooseJavaVersion(detectedJavaVersion, gradleVersion);
|
|
1070
|
-
if (choice.kind === "satisfies" || choice.kind === "unknown")
|
|
1071
|
-
return undefined;
|
|
1072
|
-
const { declared, ceiling, installed } = choice;
|
|
1073
|
-
// "11, 17 and 21", not "11 and 17 and 21" — a human reads this next to a
|
|
1074
|
-
// build that will not start.
|
|
1075
|
-
const list = installed.length > 1
|
|
1076
|
-
? `${installed.slice(0, -1).join(", ")} and ${installed[installed.length - 1]}`
|
|
1077
|
-
: String(installed[0]);
|
|
1078
|
-
const asked = source ? `Java ${declared} (${source})` : `Java ${declared}`;
|
|
1079
|
-
if (choice.kind === "wrapper-below-runners") {
|
|
1080
|
-
return `Gradle ${gradleVersion} runs on Java ${ceiling} at most, and build machines carry only ${list}. This pipeline will very likely fail before it compiles anything — upgrade the Gradle wrapper.`;
|
|
1081
|
-
}
|
|
1082
|
-
if (choice.kind === "above-runners") {
|
|
1083
|
-
return `This project declares Java ${declared}, and build machines carry only ${list}. Leaving JAVA_VERSION at the pipeline default — asking for ${declared} would fail at "Set the Java version", and quietly choosing a lower one would be a guess.`;
|
|
1084
|
-
}
|
|
1085
|
-
// capped-by-wrapper
|
|
1086
|
-
const preamble = declared
|
|
1087
|
-
? `This project asks for ${asked}, and Gradle ${gradleVersion} runs on Java ${ceiling} at most.`
|
|
1088
|
-
: `Gradle ${gradleVersion} runs on Java ${ceiling} at most.`;
|
|
1089
|
-
return `${preamble} Building on ${choice.version} — the newest the wrapper allows.`;
|
|
1090
|
-
}
|
|
1091
|
-
// ---------------------------------------------------------------------------
|
|
1092
|
-
// Gitignore helper
|
|
1093
|
-
// ---------------------------------------------------------------------------
|
|
1094
|
-
const CI_RELEVANT_GITIGNORE_PATTERNS = [
|
|
1095
|
-
{ pattern: /google-services\.json/, label: "google-services.json" },
|
|
1096
|
-
{ pattern: /local\.properties/, label: "local.properties" },
|
|
1097
|
-
{ pattern: /keystore\.properties/, label: "keystore.properties" },
|
|
1098
|
-
{ pattern: /\.jks$|\.keystore$|\*\.jks|\*\.keystore/, label: "keystore files (*.jks / *.keystore)" },
|
|
1099
|
-
{ pattern: /\.env$|\*\.env/, label: ".env files" },
|
|
1100
|
-
];
|
|
1101
|
-
function checkGitignore(root) {
|
|
1102
|
-
const gitignorePath = resolve(root, ".gitignore");
|
|
1103
|
-
if (!existsSync(gitignorePath))
|
|
1104
|
-
return [];
|
|
1105
|
-
const content = safeRead(gitignorePath);
|
|
1106
|
-
const lines = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
1107
|
-
const found = [];
|
|
1108
|
-
for (const { pattern, label } of CI_RELEVANT_GITIGNORE_PATTERNS) {
|
|
1109
|
-
if (lines.some((l) => pattern.test(l))) {
|
|
1110
|
-
found.push(label);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
return found;
|
|
1114
|
-
}
|
|
1115
|
-
// ---------------------------------------------------------------------------
|
|
1116
|
-
// Build variant detection
|
|
1117
|
-
// ---------------------------------------------------------------------------
|
|
1118
|
-
/**
|
|
1119
|
-
* Extracts the inner content of a named block using brace-counting.
|
|
1120
|
-
* e.g. extractBlockContent(content, "buildTypes") returns everything inside buildTypes { ... }.
|
|
1121
|
-
* Returns undefined if the block is not found.
|
|
1122
|
-
*/
|
|
1123
|
-
function extractBlockContent(content, keyword) {
|
|
1124
|
-
const keywordRe = new RegExp(`\\b${keyword}\\s*\\{`);
|
|
1125
|
-
const match = keywordRe.exec(content);
|
|
1126
|
-
if (!match)
|
|
1127
|
-
return undefined;
|
|
1128
|
-
const start = content.indexOf("{", match.index);
|
|
1129
|
-
let depth = 0;
|
|
1130
|
-
for (let i = start; i < content.length; i++) {
|
|
1131
|
-
if (content[i] === "{")
|
|
1132
|
-
depth++;
|
|
1133
|
-
else if (content[i] === "}") {
|
|
1134
|
-
depth--;
|
|
1135
|
-
if (depth === 0)
|
|
1136
|
-
return content.slice(start + 1, i);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
return undefined;
|
|
1140
|
-
}
|
|
1141
|
-
const CONTROL_KEYWORDS = new Set([
|
|
1142
|
-
"if", "else", "for", "while", "when", "try", "catch", "finally",
|
|
1143
|
-
"return", "val", "var", "fun", "object", "class", "interface",
|
|
1144
|
-
]);
|
|
1145
|
-
/**
|
|
1146
|
-
* Extracts the names of direct child blocks from block content.
|
|
1147
|
-
* Handles both Groovy DSL (name { }) and Kotlin DSL (create("name") { } / getByName("name") { }).
|
|
1148
|
-
*/
|
|
1149
|
-
function extractDirectChildNames(block) {
|
|
1150
|
-
const names = [];
|
|
1151
|
-
let depth = 0;
|
|
1152
|
-
for (const line of block.split("\n")) {
|
|
1153
|
-
const opens = (line.match(/\{/g) || []).length;
|
|
1154
|
-
const closes = (line.match(/\}/g) || []).length;
|
|
1155
|
-
if (depth === 0 && opens > 0) {
|
|
1156
|
-
// Kotlin DSL: create("name") { or getByName("name") {
|
|
1157
|
-
const kotlinMatch = /(?:create|getByName)\(\s*["'](\w+)["']\s*\)/.exec(line);
|
|
1158
|
-
if (kotlinMatch) {
|
|
1159
|
-
names.push(kotlinMatch[1]);
|
|
1160
|
-
}
|
|
1161
|
-
else {
|
|
1162
|
-
// Groovy DSL: name { (identifier followed by optional whitespace then {)
|
|
1163
|
-
const groovyMatch = /^\s*([a-z]\w*)\s*(?:\.\w+\s*)?\{/.exec(line);
|
|
1164
|
-
if (groovyMatch && !CONTROL_KEYWORDS.has(groovyMatch[1])) {
|
|
1165
|
-
names.push(groovyMatch[1]);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
depth += opens - closes;
|
|
1170
|
-
if (depth < 0)
|
|
1171
|
-
depth = 0;
|
|
1172
|
-
}
|
|
1173
|
-
return [...new Set(names)];
|
|
1174
|
-
}
|
|
1175
|
-
/** Source-set directories AGP defines itself, which never name a flavor. */
|
|
1176
|
-
const SHARED_SOURCE_SETS = new Set(["main", "test", "androidTest", "testFixtures"]);
|
|
1177
|
-
/** `testProd` and `androidTestProd` are `prod`'s test source sets, not flavors. */
|
|
1178
|
-
const TEST_SOURCE_SET_PREFIXES = ["test", "androidTest"];
|
|
1179
|
-
/**
|
|
1180
|
-
* What a source set holds. A directory under `src/` carrying none of these is
|
|
1181
|
-
* something a human left there, not a source set AGP would read.
|
|
1182
|
-
*/
|
|
1183
|
-
const SOURCE_SET_CONTENTS = new Set([
|
|
1184
|
-
"java",
|
|
1185
|
-
"kotlin",
|
|
1186
|
-
"res",
|
|
1187
|
-
"resources",
|
|
1188
|
-
"assets",
|
|
1189
|
-
"aidl",
|
|
1190
|
-
"jni",
|
|
1191
|
-
"jniLibs",
|
|
1192
|
-
"rs",
|
|
1193
|
-
"renderscript",
|
|
1194
|
-
"shaders",
|
|
1195
|
-
"ml",
|
|
1196
|
-
"baselineProfiles",
|
|
1197
|
-
"AndroidManifest.xml",
|
|
1198
|
-
"google-services.json",
|
|
1199
|
-
]);
|
|
1200
|
-
function safeReaddir(dir) {
|
|
1201
|
-
try {
|
|
1202
|
-
return readdirSync(dir, { withFileTypes: true }).map((e) => ({
|
|
1203
|
-
name: e.name,
|
|
1204
|
-
isDirectory: e.isDirectory(),
|
|
1205
|
-
}));
|
|
1206
|
-
}
|
|
1207
|
-
catch {
|
|
1208
|
-
return [];
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
/** `testProd` starts with the segment `test`; a flavor called `testing` does not. */
|
|
1212
|
-
function startsWithSegment(name, segment) {
|
|
1213
|
-
if (!name.startsWith(segment) || name.length === segment.length)
|
|
1214
|
-
return false;
|
|
1215
|
-
const next = name[segment.length];
|
|
1216
|
-
return next === next.toUpperCase() && next !== next.toLowerCase();
|
|
1217
|
-
}
|
|
1218
|
-
/** `ciDynamicDebug` ends with the segment `debug`; `staging` does not. */
|
|
1219
|
-
function endsWithSegment(name, segment) {
|
|
1220
|
-
const capitalized = segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1221
|
-
return name.length > capitalized.length && name.endsWith(capitalized);
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Product flavors read from the source sets a module has on disk.
|
|
1225
|
-
*
|
|
1226
|
-
* Modern Android declares its flavors in a convention plugin — an enum in
|
|
1227
|
-
* Kotlin source, applied through `create(flavor.name)` — so there is no
|
|
1228
|
-
* literal `productFlavors { … }` block anywhere to parse. The scan is static
|
|
1229
|
-
* by design (running a Gradle configuration phase to list flavors would cost
|
|
1230
|
-
* minutes on every `ci build`), so the evidence has to be something already on
|
|
1231
|
-
* disk. A flavor has a source set, which is the same evidence `invarn setup`
|
|
1232
|
-
* has always resolved `google-services.json` by.
|
|
1233
|
-
*
|
|
1234
|
-
* A directory under `<module>/src/` names a flavor when it is none of the
|
|
1235
|
-
* things AGP creates for its own reasons: the shared and test source sets, a
|
|
1236
|
-
* build type, a test source set for one flavor, or a variant — a flavor and a
|
|
1237
|
-
* build type already joined, which taking for a flavor would cross with the
|
|
1238
|
-
* build types a second time and offer `ciDynamicDebugDebug`.
|
|
1239
|
-
*/
|
|
1240
|
-
function flavorsFromSourceSets(gradleFiles, buildTypes) {
|
|
1241
|
-
const found = new Set();
|
|
1242
|
-
for (const filePath of gradleFiles) {
|
|
1243
|
-
const srcDir = join(dirname(filePath), "src");
|
|
1244
|
-
for (const entry of safeReaddir(srcDir)) {
|
|
1245
|
-
if (!entry.isDirectory)
|
|
1246
|
-
continue;
|
|
1247
|
-
const name = entry.name;
|
|
1248
|
-
if (SHARED_SOURCE_SETS.has(name))
|
|
1249
|
-
continue;
|
|
1250
|
-
if (buildTypes.includes(name))
|
|
1251
|
-
continue;
|
|
1252
|
-
if (TEST_SOURCE_SET_PREFIXES.some((p) => startsWithSegment(name, p)))
|
|
1253
|
-
continue;
|
|
1254
|
-
if (buildTypes.some((bt) => endsWithSegment(name, bt)))
|
|
1255
|
-
continue;
|
|
1256
|
-
const holds = safeReaddir(join(srcDir, name));
|
|
1257
|
-
if (!holds.some((child) => SOURCE_SET_CONTENTS.has(child.name)))
|
|
1258
|
-
continue;
|
|
1259
|
-
found.add(name);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
// The filesystem's order is not a declaration order, so give it one.
|
|
1263
|
-
return Array.from(found).sort();
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Detects build types and product flavors from Gradle files by static parsing,
|
|
1267
|
-
* falling back to the module's source sets for the flavors when no Gradle file
|
|
1268
|
-
* declares any (see `flavorsFromSourceSets`).
|
|
1269
|
-
*
|
|
1270
|
-
* Always includes "debug" and "release" as defaults.
|
|
1271
|
-
*
|
|
1272
|
-
* The fallback only ever runs when the literal scan found nothing, so a
|
|
1273
|
-
* project whose Gradle files declare flavors is decided by those files exactly
|
|
1274
|
-
* as before. Merging the two would let a leftover directory invent a flavor
|
|
1275
|
-
* the build does not have.
|
|
1276
|
-
*/
|
|
1277
|
-
export function detectBuildVariants(gradleFiles) {
|
|
1278
|
-
const buildTypeSet = new Set(["debug", "release"]);
|
|
1279
|
-
const productFlavorSet = new Set();
|
|
1280
|
-
for (const filePath of gradleFiles) {
|
|
1281
|
-
const content = safeRead(filePath);
|
|
1282
|
-
const buildTypesBlock = extractBlockContent(content, "buildTypes");
|
|
1283
|
-
if (buildTypesBlock) {
|
|
1284
|
-
for (const name of extractDirectChildNames(buildTypesBlock)) {
|
|
1285
|
-
buildTypeSet.add(name);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
const productFlavorsBlock = extractBlockContent(content, "productFlavors");
|
|
1289
|
-
if (productFlavorsBlock) {
|
|
1290
|
-
for (const name of extractDirectChildNames(productFlavorsBlock)) {
|
|
1291
|
-
productFlavorSet.add(name);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
const buildTypes = Array.from(buildTypeSet);
|
|
1296
|
-
const productFlavors = productFlavorSet.size > 0
|
|
1297
|
-
? Array.from(productFlavorSet)
|
|
1298
|
-
: flavorsFromSourceSets(gradleFiles, buildTypes);
|
|
1299
|
-
const variants = productFlavors.length === 0
|
|
1300
|
-
? buildTypes
|
|
1301
|
-
: productFlavors.flatMap((flavor) => buildTypes.map((bt) => `${flavor}${bt.charAt(0).toUpperCase()}${bt.slice(1)}`));
|
|
1302
|
-
return { buildTypes, productFlavors, variants };
|
|
1303
|
-
}
|
|
1304
|
-
// ---------------------------------------------------------------------------
|
|
1305
|
-
// Main scanner
|
|
1306
|
-
// ---------------------------------------------------------------------------
|
|
1307
|
-
export async function scanAndroidProject(projectRoot) {
|
|
1308
|
-
const warnings = [];
|
|
1309
|
-
// Track env vars we've already warned about to avoid duplicates
|
|
1310
|
-
const seenEnvVars = new Set();
|
|
1311
|
-
const seenProperties = new Set();
|
|
1312
|
-
// Compute gitignore items upfront — used in multiple sections below
|
|
1313
|
-
const gitignoreItems = checkGitignore(projectRoot);
|
|
1314
|
-
// ------------------------------------------------------------------
|
|
1315
|
-
// 1. Missing files
|
|
1316
|
-
// ------------------------------------------------------------------
|
|
1317
|
-
if (!existsSync(resolve(projectRoot, "local.properties"))) {
|
|
1318
|
-
warnings.push({
|
|
1319
|
-
category: "missing-file",
|
|
1320
|
-
severity: "warning",
|
|
1321
|
-
message: "local.properties not found",
|
|
1322
|
-
hint: "CI needs ANDROID_HOME set as an env var, or sdk.dir in local.properties",
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
// ------------------------------------------------------------------
|
|
1326
|
-
// 2. Detect required Java version and build variants
|
|
1327
|
-
// ------------------------------------------------------------------
|
|
1328
|
-
const gradleFiles = findGradleFiles(projectRoot);
|
|
1329
|
-
const declaredJavaVersion = detectRequiredJavaVersion(gradleFiles);
|
|
1330
|
-
// A version file or a workflow pin is a floor like any declaration, and for
|
|
1331
|
-
// a project that declares nothing in Gradle it is the only thing that says
|
|
1332
|
-
// which JDK the build needs.
|
|
1333
|
-
const pinnedJava = detectPinnedJavaVersion(projectRoot);
|
|
1334
|
-
const detectedJavaVersion = declaredJavaVersion === undefined && pinnedJava === undefined
|
|
1335
|
-
? undefined
|
|
1336
|
-
: Math.max(declaredJavaVersion ?? 0, pinnedJava?.version ?? 0);
|
|
1337
|
-
// Only when the pin is what set the level. A Gradle file asking for as much
|
|
1338
|
-
// or more is the thing a reader would find by looking, so naming the
|
|
1339
|
-
// workflow there would point away from the answer.
|
|
1340
|
-
const detectedJavaVersionSource = pinnedJava !== undefined && pinnedJava.version > (declaredJavaVersion ?? 0)
|
|
1341
|
-
? pinnedJava.source
|
|
1342
|
-
: undefined;
|
|
1343
|
-
const detectedGradleVersion = detectGradleWrapperVersion(projectRoot);
|
|
1344
|
-
const buildVariants = detectBuildVariants(gradleFiles);
|
|
1345
|
-
// ------------------------------------------------------------------
|
|
1346
|
-
// 3. Scan Gradle files
|
|
1347
|
-
// ------------------------------------------------------------------
|
|
1348
|
-
let gmsDetected = false;
|
|
1349
|
-
let gmsDetectedIn = "";
|
|
1350
|
-
let signingDetected = false;
|
|
1351
|
-
let signingDetectedIn = "";
|
|
1352
|
-
for (const filePath of gradleFiles) {
|
|
1353
|
-
const content = safeRead(filePath);
|
|
1354
|
-
const rel = relPath(projectRoot, filePath);
|
|
1355
|
-
// GMS plugin
|
|
1356
|
-
if (detectGmsPlugin(content)) {
|
|
1357
|
-
gmsDetected = true;
|
|
1358
|
-
gmsDetectedIn = rel;
|
|
1359
|
-
}
|
|
1360
|
-
// Signing config
|
|
1361
|
-
if (detectSigningConfigs(content)) {
|
|
1362
|
-
signingDetected = true;
|
|
1363
|
-
signingDetectedIn = rel;
|
|
1364
|
-
// Keystore file paths referenced
|
|
1365
|
-
for (const kPath of extractKeystorePaths(content)) {
|
|
1366
|
-
const resolved = resolve(projectRoot, kPath);
|
|
1367
|
-
if (!existsSync(resolved)) {
|
|
1368
|
-
warnings.push({
|
|
1369
|
-
category: "missing-file",
|
|
1370
|
-
severity: "warning",
|
|
1371
|
-
message: `Keystore file not found: ${kPath}`,
|
|
1372
|
-
hint: "Provide the keystore file as a secret file in CI or update the path in signingConfigs",
|
|
1373
|
-
location: rel,
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
// Signing credential env var / property refs
|
|
1378
|
-
for (const ref of parseSigningCredentialRefs(content)) {
|
|
1379
|
-
if (!seenEnvVars.has(ref.name)) {
|
|
1380
|
-
seenEnvVars.add(ref.name);
|
|
1381
|
-
warnings.push({
|
|
1382
|
-
category: "signing-config",
|
|
1383
|
-
severity: "warning",
|
|
1384
|
-
message: `Signing credential referenced: ${ref.name}`,
|
|
1385
|
-
hint: `Add via: ci secrets add ${ref.name}`,
|
|
1386
|
-
location: `${rel}:${ref.line}`,
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
// System.getenv() references (outside buildConfigField — caught separately)
|
|
1392
|
-
for (const ref of extractEnvVarRefs(content)) {
|
|
1393
|
-
if (!seenEnvVars.has(ref.name)) {
|
|
1394
|
-
seenEnvVars.add(ref.name);
|
|
1395
|
-
warnings.push({
|
|
1396
|
-
category: "env-var",
|
|
1397
|
-
severity: "warning",
|
|
1398
|
-
message: `Environment variable referenced: ${ref.name}`,
|
|
1399
|
-
hint: `Add via: ci secrets add ${ref.name}`,
|
|
1400
|
-
location: `${rel}:${ref.line}`,
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
// buildConfigField + System.getenv references
|
|
1405
|
-
for (const ref of extractBuildConfigEnvRefs(content)) {
|
|
1406
|
-
if (!seenEnvVars.has(ref.name)) {
|
|
1407
|
-
seenEnvVars.add(ref.name);
|
|
1408
|
-
warnings.push({
|
|
1409
|
-
category: "build-config",
|
|
1410
|
-
severity: "warning",
|
|
1411
|
-
message: `BuildConfig env var referenced: ${ref.name}`,
|
|
1412
|
-
hint: `Add via: ci secrets add ${ref.name}`,
|
|
1413
|
-
location: `${rel}:${ref.line}`,
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
// Gradle property references
|
|
1418
|
-
for (const ref of extractPropertyRefs(content)) {
|
|
1419
|
-
if (!seenProperties.has(ref.name)) {
|
|
1420
|
-
seenProperties.add(ref.name);
|
|
1421
|
-
warnings.push({
|
|
1422
|
-
category: "gradle-property",
|
|
1423
|
-
severity: "warning",
|
|
1424
|
-
message: `Gradle property referenced: ${ref.name}`,
|
|
1425
|
-
hint: "Define in gradle.properties or pass as -P flag on CI",
|
|
1426
|
-
location: `${rel}:${ref.line}`,
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
// ------------------------------------------------------------------
|
|
1432
|
-
// 4. Google Secrets Gradle Plugin — detect missing local.properties
|
|
1433
|
-
// ------------------------------------------------------------------
|
|
1434
|
-
//
|
|
1435
|
-
// The Secrets plugin reads a properties file (default: local.properties) and
|
|
1436
|
-
// auto-generates BuildConfig fields. If that file is gitignored / absent in CI
|
|
1437
|
-
// the fields are not generated, causing "Unresolved reference" compile errors.
|
|
1438
|
-
// We check once per unique properties file path across all modules.
|
|
1439
|
-
// ------------------------------------------------------------------
|
|
1440
|
-
const seenSecretsPropertiesFiles = new Set();
|
|
1441
|
-
// First pass: collect the defaultPropertiesFileName from any build file that sets it.
|
|
1442
|
-
// This is typically "secrets.defaults.properties" in the app module.
|
|
1443
|
-
let secretsDefaultsFileName;
|
|
1444
|
-
for (const filePath of gradleFiles) {
|
|
1445
|
-
const content = safeRead(filePath);
|
|
1446
|
-
const block = extractBlockContent(content, "secrets");
|
|
1447
|
-
if (!block)
|
|
1448
|
-
continue;
|
|
1449
|
-
const m = /defaultPropertiesFileName\s*=\s*["']([^"']+)["']/.exec(block);
|
|
1450
|
-
if (m) {
|
|
1451
|
-
secretsDefaultsFileName = m[1];
|
|
1452
|
-
break;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
// Also look for any *.defaults.properties file in the project root as a fallback
|
|
1456
|
-
if (!secretsDefaultsFileName) {
|
|
1457
|
-
const candidates = ["secrets.defaults.properties", "local.defaults.properties"];
|
|
1458
|
-
for (const c of candidates) {
|
|
1459
|
-
if (existsSync(resolve(projectRoot, c))) {
|
|
1460
|
-
secretsDefaultsFileName = c;
|
|
1461
|
-
break;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
for (const filePath of gradleFiles) {
|
|
1466
|
-
const content = safeRead(filePath);
|
|
1467
|
-
if (!detectSecretsPlugin(content))
|
|
1468
|
-
continue;
|
|
1469
|
-
const propsFileName = extractSecretsPropertiesFile(content);
|
|
1470
|
-
if (seenSecretsPropertiesFiles.has(propsFileName))
|
|
1471
|
-
continue;
|
|
1472
|
-
seenSecretsPropertiesFiles.add(propsFileName);
|
|
1473
|
-
// Warn when the properties file is gitignored — it won't be present on a clean CI machine
|
|
1474
|
-
// even if it exists locally (e.g. local.properties with only sdk.dir).
|
|
1475
|
-
const isGitignored = gitignoreItems.some((item) => item.includes(propsFileName));
|
|
1476
|
-
if (!isGitignored)
|
|
1477
|
-
continue;
|
|
1478
|
-
// Find expected keys from the companion defaults file
|
|
1479
|
-
const expectedKeys = secretsDefaultsFileName
|
|
1480
|
-
? parsePropertyKeys(safeRead(resolve(projectRoot, secretsDefaultsFileName)))
|
|
1481
|
-
: [];
|
|
1482
|
-
if (expectedKeys.length > 0) {
|
|
1483
|
-
// Emit one warning per expected key so it shows up in the missing-secrets hint
|
|
1484
|
-
for (const key of expectedKeys) {
|
|
1485
|
-
warnings.push({
|
|
1486
|
-
category: "secrets-plugin",
|
|
1487
|
-
severity: "warning",
|
|
1488
|
-
message: `Secrets plugin key: ${key}`,
|
|
1489
|
-
hint: `Add via: ci secrets add ${key}`,
|
|
1490
|
-
location: propsFileName,
|
|
1491
|
-
});
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
else {
|
|
1495
|
-
warnings.push({
|
|
1496
|
-
category: "secrets-plugin",
|
|
1497
|
-
severity: "warning",
|
|
1498
|
-
message: `Google Secrets Gradle Plugin reads '${propsFileName}' which is absent in CI`,
|
|
1499
|
-
hint: `Inject '${propsFileName}' as a file step using a secret variable`,
|
|
1500
|
-
location: relPath(projectRoot, filePath),
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
// ------------------------------------------------------------------
|
|
1505
|
-
// 4b. Which keys that properties file has to declare
|
|
1506
|
-
// ------------------------------------------------------------------
|
|
1507
|
-
//
|
|
1508
|
-
// Section 4 says the file is absent; this says what is in it. The two are
|
|
1509
|
-
// separate because they answer different questions for different readers —
|
|
1510
|
-
// the warnings above tell a person a file is missing, and this tells a
|
|
1511
|
-
// generator what a stand-in for it has to contain.
|
|
1512
|
-
// ------------------------------------------------------------------
|
|
1513
|
-
const missingPropertyKeys = collectPropertyKeys(gradleFiles);
|
|
1514
|
-
// ------------------------------------------------------------------
|
|
1515
|
-
// 5. Firebase / GMS — check google-services.json after scanning all files
|
|
1516
|
-
// ------------------------------------------------------------------
|
|
1517
|
-
if (gmsDetected) {
|
|
1518
|
-
// google-services.json can live in the root or in the app module dir
|
|
1519
|
-
const candidatePaths = [
|
|
1520
|
-
resolve(projectRoot, "google-services.json"),
|
|
1521
|
-
resolve(projectRoot, "app", "google-services.json"),
|
|
1522
|
-
];
|
|
1523
|
-
const found = candidatePaths.some((p) => existsSync(p));
|
|
1524
|
-
if (!found) {
|
|
1525
|
-
warnings.push({
|
|
1526
|
-
category: "firebase",
|
|
1527
|
-
severity: "warning",
|
|
1528
|
-
message: `google-services.json not found (GMS plugin detected in ${gmsDetectedIn})`,
|
|
1529
|
-
hint: "Add google-services.json as a secret file or generate it from env vars at build time",
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
// ------------------------------------------------------------------
|
|
1534
|
-
// 5. Signing config summary warning (if detected and not already specific)
|
|
1535
|
-
// ------------------------------------------------------------------
|
|
1536
|
-
if (signingDetected) {
|
|
1537
|
-
warnings.push({
|
|
1538
|
-
category: "signing-config",
|
|
1539
|
-
severity: "info",
|
|
1540
|
-
message: `signingConfigs block found in ${signingDetectedIn}`,
|
|
1541
|
-
hint: "Ensure keystore file and all signing credentials are available in the CI environment",
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
// ------------------------------------------------------------------
|
|
1545
|
-
// 6. Gitignore cross-check
|
|
1546
|
-
// ------------------------------------------------------------------
|
|
1547
|
-
for (const item of gitignoreItems) {
|
|
1548
|
-
// Only warn if we haven't already emitted a more specific warning for it
|
|
1549
|
-
const alreadyCovered = (item === "google-services.json" && gmsDetected) ||
|
|
1550
|
-
(item === "local.properties" && !existsSync(resolve(projectRoot, "local.properties")));
|
|
1551
|
-
if (!alreadyCovered) {
|
|
1552
|
-
warnings.push({
|
|
1553
|
-
category: "missing-file",
|
|
1554
|
-
severity: "info",
|
|
1555
|
-
message: `${item} is in .gitignore — likely absent in CI`,
|
|
1556
|
-
hint: "Provide this file as a secret or generate it at build time",
|
|
1557
|
-
});
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
return {
|
|
1561
|
-
warnings,
|
|
1562
|
-
detectedJavaVersion,
|
|
1563
|
-
detectedJavaVersionSource,
|
|
1564
|
-
detectedGradleVersion,
|
|
1565
|
-
buildVariants,
|
|
1566
|
-
missingPropertyKeys,
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
// ---------------------------------------------------------------------------
|
|
1570
|
-
// Formatter
|
|
1571
|
-
// ---------------------------------------------------------------------------
|
|
1572
|
-
const CATEGORY_LABELS = {
|
|
1573
|
-
"missing-file": "Missing files",
|
|
1574
|
-
"env-var": "Environment variables referenced in Gradle files",
|
|
1575
|
-
"gradle-property": "Gradle properties referenced",
|
|
1576
|
-
"signing-config": "Signing configuration",
|
|
1577
|
-
"build-config": "BuildConfig env vars",
|
|
1578
|
-
"firebase": "Firebase / GMS",
|
|
1579
|
-
"secrets-plugin": "Google Secrets Gradle Plugin (local.properties keys)",
|
|
1580
|
-
};
|
|
1581
|
-
export function formatScanResult(result) {
|
|
1582
|
-
const lines = [];
|
|
1583
|
-
const sep = "─".repeat(50);
|
|
1584
|
-
// Always show the detected Java version, and the one the pipeline will
|
|
1585
|
-
// actually name — they differ whenever the declaration is below the oldest
|
|
1586
|
-
// installed JDK or above the newest, and saying only the first made the
|
|
1587
|
-
// second look like it had been honoured.
|
|
1588
|
-
//
|
|
1589
|
-
// And when they differ, say why in the same breath. An answer that is not
|
|
1590
|
-
// what was asked for, printed with no reason, reads as a fact about the
|
|
1591
|
-
// project rather than as a decision made on its behalf.
|
|
1592
|
-
if (result.detectedJavaVersion !== undefined) {
|
|
1593
|
-
const willUse = runnableJavaVersion(result.detectedJavaVersion, result.detectedGradleVersion);
|
|
1594
|
-
lines.push(willUse === undefined
|
|
1595
|
-
? `ℹ Detected Java ${result.detectedJavaVersion} requirement — no installed JDK can satisfy it`
|
|
1596
|
-
: `ℹ Detected Java ${result.detectedJavaVersion} requirement — pipeline will use Java ${willUse}`);
|
|
1597
|
-
const problem = javaVersionProblem(result.detectedJavaVersion, result.detectedGradleVersion, result.detectedJavaVersionSource);
|
|
1598
|
-
if (problem)
|
|
1599
|
-
lines.push(` ${problem}`);
|
|
1600
|
-
lines.push("");
|
|
1601
|
-
}
|
|
1602
|
-
if (result.warnings.length === 0) {
|
|
1603
|
-
lines.push("✅ No potential unknowns detected.");
|
|
1604
|
-
return lines.join("\n") + "\n";
|
|
1605
|
-
}
|
|
1606
|
-
lines.push(`⚠ Android Project Scan — Potential Unknowns`);
|
|
1607
|
-
lines.push(sep);
|
|
1608
|
-
lines.push("");
|
|
1609
|
-
// Group by category
|
|
1610
|
-
const byCategory = new Map();
|
|
1611
|
-
for (const w of result.warnings) {
|
|
1612
|
-
if (!byCategory.has(w.category))
|
|
1613
|
-
byCategory.set(w.category, []);
|
|
1614
|
-
byCategory.get(w.category).push(w);
|
|
1615
|
-
}
|
|
1616
|
-
for (const [cat, items] of byCategory) {
|
|
1617
|
-
lines.push(`${CATEGORY_LABELS[cat]}:`);
|
|
1618
|
-
for (const w of items) {
|
|
1619
|
-
const icon = w.severity === "warning" ? "⚠ " : "ℹ ";
|
|
1620
|
-
const loc = w.location ? ` (${w.location})` : "";
|
|
1621
|
-
lines.push(` ${icon} ${w.message}${loc}`);
|
|
1622
|
-
if (w.hint) {
|
|
1623
|
-
lines.push(` Hint: ${w.hint}`);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
lines.push("");
|
|
1627
|
-
}
|
|
1628
|
-
lines.push(sep);
|
|
1629
|
-
const warnCount = result.warnings.filter((w) => w.severity === "warning").length;
|
|
1630
|
-
const infoCount = result.warnings.filter((w) => w.severity === "info").length;
|
|
1631
|
-
const parts = [];
|
|
1632
|
-
if (warnCount > 0)
|
|
1633
|
-
parts.push(`${warnCount} warning(s)`);
|
|
1634
|
-
if (infoCount > 0)
|
|
1635
|
-
parts.push(`${infoCount} info`);
|
|
1636
|
-
lines.push(`${parts.join(", ")} found. Review before running on CI.`);
|
|
1637
|
-
return lines.join("\n");
|
|
1638
|
-
}
|
|
1639
|
-
//# sourceMappingURL=android-scanner.js.map
|