From dcf29bc3a6b26f80ddf5d1798d2be8a171d7512e Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Tue, 3 Mar 2026 09:36:09 +0000 Subject: [PATCH] feat: add platform detection for Heroku, Platform.sh, and Upsun Add a `platforms` ruleset detecting cloud hosting platforms: - Heroku: via heroku.yml (definitive), app.json with buildpacks/addons (definitive), or Procfile alone (maybe) - Platform.sh: via .platform.app.yaml/.yml or .platform/applications.yaml/.yml - Upsun: via .upsun/*.yaml or .upsun/*.yml The `heroku-procfile` maybe result is suppressed when a definitive heroku result already exists for the same path. Adds test fixtures for heroku-app, heroku-docker, and upsun-app directories, plus assertions for the existing configured-app (Platform.sh) fixture. Refs: AI-147 Co-Authored-By: Claude Opus 4.6 (1M context) --- config/platforms.yml | 56 +++++++++++++++++++++++++++++ docs/functions.md | 60 ++++++++++++++++---------------- expr.cache | 5 +++ pkg/rules/analyze_testfs_test.go | 30 ++++++++++++++++ 4 files changed, 121 insertions(+), 30 deletions(-) create mode 100644 config/platforms.yml diff --git a/config/platforms.yml b/config/platforms.yml new file mode 100644 index 0000000..ddd80a7 --- /dev/null +++ b/config/platforms.yml @@ -0,0 +1,56 @@ +platforms: + rules: + # Heroku: heroku.yml is 100% unique to Heroku (Docker-based deploys). + heroku-yml: + when: fs.fileExists("heroku.yml") + then: heroku + group: cloud + read_files: [Procfile, app.json, heroku.yml, runtime.txt, .python-version] + + # Heroku: app.json with Heroku-specific keys (buildpacks, addons). + heroku-app-json: + when: >- + fs.fileExists("app.json") + && (fs.fileContains("app.json", '"buildpacks"') + || fs.fileContains("app.json", '"addons"')) + then: heroku + group: cloud + read_files: [Procfile, app.json, heroku.yml, runtime.txt, .python-version] + + # Heroku: Procfile alone is ambiguous (Dokku, Railway, Foreman). + # maybe results are filtered from digest/CLI output. + heroku-procfile: + when: fs.fileExists("Procfile") + maybe: heroku + group: cloud + read_files: [Procfile, app.json, heroku.yml, runtime.txt, .python-version] + + # Platform.sh: per-app config or unified config. + platformsh: + when: >- + fs.fileExists(".platform.app.yaml") + || fs.fileExists(".platform.app.yml") + || fs.fileExists(".platform/applications.yaml") + || fs.fileExists(".platform/applications.yml") + then: platformsh + group: cloud + read_files: + - .platform.app.yaml + - .platform.app.yml + - .platform/applications.yaml + - .platform/applications.yml + - .platform/services.yaml + - .platform/services.yml + - .platform/routes.yaml + - .platform/routes.yml + + # Upsun: config in .upsun/ directory. + upsun: + when: >- + fs.glob(".upsun/*.yaml").size() > 0 + || fs.glob(".upsun/*.yml").size() > 0 + then: upsun + group: cloud + read_files: + - .upsun/config.yaml + - .upsun/config.yml diff --git a/docs/functions.md b/docs/functions.md index 0193491..a911e91 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -64,8 +64,8 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `@in` -* `@in(A, list(A))` -> `bool` -* `@in(A, map(A, B))` -> `bool` +* `@in(, list())` -> `bool` +* `@in(, map(, ))` -> `bool` ### `@not_strictly_false` @@ -73,14 +73,14 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `@sortByAssociatedKeys` -* `.@sortByAssociatedKeys(list(int))` -> `list(T)` -* `.@sortByAssociatedKeys(list(uint))` -> `list(T)` -* `.@sortByAssociatedKeys(list(double))` -> `list(T)` -* `.@sortByAssociatedKeys(list(bool))` -> `list(T)` -* `.@sortByAssociatedKeys(list(google.protobuf.Duration))` -> `list(T)` -* `.@sortByAssociatedKeys(list(google.protobuf.Timestamp))` -> `list(T)` -* `.@sortByAssociatedKeys(list(string))` -> `list(T)` -* `.@sortByAssociatedKeys(list(bytes))` -> `list(T)` +* `)>.@sortByAssociatedKeys(list(int))` -> `list()` +* `)>.@sortByAssociatedKeys(list(uint))` -> `list()` +* `)>.@sortByAssociatedKeys(list(double))` -> `list()` +* `)>.@sortByAssociatedKeys(list(bool))` -> `list()` +* `)>.@sortByAssociatedKeys(list(google.protobuf.Duration))` -> `list()` +* `)>.@sortByAssociatedKeys(list(google.protobuf.Timestamp))` -> `list()` +* `)>.@sortByAssociatedKeys(list(string))` -> `list()` +* `)>.@sortByAssociatedKeys(list(bytes))` -> `list()` ### `bool` @@ -102,7 +102,7 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `distinct` -* `.distinct()` -> `list(T)` +* `)>.distinct()` -> `list()` ### `double` @@ -119,7 +119,7 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `dyn` -* `dyn(A)` -> `dyn` +* `dyn()` -> `dyn` ### `endsWith` @@ -127,7 +127,7 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `flatten` -* `.flatten()` -> `list(T)` +* `))>.flatten()` -> `list()` * `.flatten(int)` -> `list(dyn)` ### `format` @@ -190,8 +190,8 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `in` -* `in(A, list(A))` -> `bool` -* `in(A, map(A, B))` -> `bool` +* `in(, list())` -> `bool` +* `in(, map(, ))` -> `bool` ### `indexOf` @@ -237,23 +237,23 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `reverse` -* `.reverse()` -> `list(T)` +* `)>.reverse()` -> `list()` * `.reverse()` -> `string` ### `size` * `size(bytes)` -> `int` * `.size()` -> `int` -* `size(list(A))` -> `int` -* `.size()` -> `int` -* `size(map(A, B))` -> `int` -* `.size()` -> `int` +* `size(list())` -> `int` +* `)>.size()` -> `int` +* `size(map(, ))` -> `int` +* `, )>.size()` -> `int` * `size(string)` -> `int` * `.size()` -> `int` ### `slice` -* `.slice(int, int)` -> `list(T)` +* `)>.slice(int, int)` -> `list()` ### `sort` @@ -307,7 +307,7 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `type` -* `type(A)` -> `type(A)` +* `type()` -> `type()` ### `uint` @@ -333,7 +333,7 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `!=` -* `A` `!=` `A` -> `bool` +* `` `!=` `` -> `bool` ### `%` @@ -358,7 +358,7 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). * `google.protobuf.Duration` `+` `google.protobuf.Timestamp` -> `google.protobuf.Timestamp` * `google.protobuf.Timestamp` `+` `google.protobuf.Duration` -> `google.protobuf.Timestamp` * `int` `+` `int` -> `int` -* `list(A)` `+` `list(A)` -> `list(A)` +* `list()` `+` `list()` -> `list()` * `string` `+` `string` -> `string` * `uint` `+` `uint` -> `uint` @@ -413,7 +413,7 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `==` -* `A` `==` `A` -> `bool` +* `` `==` `` -> `bool` ### `>=` @@ -451,12 +451,12 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `? :` -* `bool` `?` `A` `:` `A` -> `A` +* `bool` `?` `` `:` `` -> `` ### `[ ]` -* `list(A)` `[` `int` `]` -> `A` -* `map(A, B)` `[` `A` `]` -> `B` +* `list()` `[` `int` `]` -> `` +* `map(, )` `[` `` `]` -> `` ### `not strictly false` @@ -464,8 +464,8 @@ Query YAML bytes (e.g. file contents) using YQ (same syntax as JQ). ### `in` -* `A` `in` `list(A)` -> `bool` -* `A` `in` `map(A, B)` -> `bool` +* `` `in` `list()` -> `bool` +* `` `in` `map(, )` -> `bool` ### `||` diff --git a/expr.cache b/expr.cache index c04116b..5c61d2d 100644 --- a/expr.cache +++ b/expr.cache @@ -155,12 +155,15 @@ fs.depVersion("rust", "warp") EhMIAhIPGg1mcy5kZXBWZXJzaW9uEggIARIECgJmcxoGCAISAh fs.depVersion("rust", "yew") EggIARIECgJmcxITCAISDxoNZnMuZGVwVmVyc2lvbhoGCAQSAhgFGgYIARICCgAaBggCEgIYBRoGCAMSAhgFIjEQAjItCggQASIECgJmcxIKZGVwVmVyc2lvbhoKEAMaBjIEcnVzdBoJEAQaBTIDeWV3KiQSBzxpbnB1dD4aAR0iBAgBEAAiBAgCEA0iBAgDEA4iBAgEEBY= fs.fileExists(".eleventy.js") || fs.glob("eleventy.config.*s").size() > 0 || fs.depExists("js", "@11ty/eleventy") EggIBBIECgJmcxIPCAcSCxoJbGlzdF9zaXplEhAIDxIMGgpsb2dpY2FsX29yEg0IBRIJGgdmcy5nbG9iEhMICBIPGg1ncmVhdGVyX2ludDY0EggICxIECgJmcxISCAwSDhoMZnMuZGVwRXhpc3RzEggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxIQCAoSDBoKbG9naWNhbF9vchoGCAoSAhgBGgYIDxICGAEaBggEEgIKABoGCAMSAhgFGgYICRICGAIaBggCEgIYARoGCAsSAgoAGgYIDBICGAEaBggGEgIYBRoKCAUSBjIECgIYBRoGCAcSAhgCGgYIDRICGAUaBggBEgIKABoGCAgSAhgBGgYIDhICGAUi0wEQDzLOARIEX3x8XxqKARAKMoUBEgRffHxfGi4QAjIqCggQASIECgJmcxIKZmlsZUV4aXN0cxoSEAMaDjIMLmVsZXZlbnR5LmpzGk0QCDJJEgNfPl8aOhAHMjYKLhAFMioKCBAEIgQKAmZzEgRnbG9iGhgQBhoUMhJlbGV2ZW50eS5jb25maWcuKnMSBHNpemUaBhAJGgIYABo5EAwyNQoIEAsiBAoCZnMSCWRlcEV4aXN0cxoIEA0aBDICanMaFBAOGhAyDkAxMXR5L2VsZXZlbnR5KmYSBzxpbnB1dD4aAXIiBAgKEB4iBAgMEFkiBAgJEEgiBAgPEEoiBAgDEA4iBAgLEE0iBAgBEAAiBAgFECgiBAgIEEYiBAgNEFoiBAgOEGAiBAgCEA0iBAgGECkiBAgEECEiBAgHEEM= fs.fileExists(".meteor/packages") EggIARIECgJmcxITCAISDxoNZnNfZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIyEAIyLgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaFhADGhIyEC5tZXRlb3IvcGFja2FnZXMqHhIHPGlucHV0PhoBIiIECAEQACIECAIQDSIECAMQDg== +fs.fileExists(".platform.app.yaml") || fs.fileExists(".platform.app.yml") || fs.fileExists(".platform/applications.yaml") || fs.fileExists(".platform/applications.yml") EhAIDxIMGgpsb2dpY2FsX29yEhAICxIMGgpsb2dpY2FsX29yEggIARIECgJmcxIQCAcSDBoKbG9naWNhbF9vchIICAgSBAoCZnMSCAgMEgQKAmZzEhMIAhIPGg1mcy5maWxlRXhpc3RzEhMIBRIPGg1mcy5maWxlRXhpc3RzEggIBBIECgJmcxITCAkSDxoNZnMuZmlsZUV4aXN0cxITCA0SDxoNZnMuZmlsZUV4aXN0cxoGCAISAhgBGgYIDRICGAEaBggFEgIYARoGCAgSAgoAGgYIAxICGAUaBggKEgIYBRoGCAkSAhgBGgYICxICGAEaBggBEgIKABoGCAYSAhgFGgYIDxICGAEaBggEEgIKABoGCAcSAhgBGgYIDhICGAUaBggMEgIKACKNAhALMogCEgRffHxfGnUQBzJxEgRffHxfGjQQAjIwCggQASIECgJmcxIKZmlsZUV4aXN0cxoYEAMaFDISLnBsYXRmb3JtLmFwcC55YW1sGjMQBTIvCggQBCIECgJmcxIKZmlsZUV4aXN0cxoXEAYaEzIRLnBsYXRmb3JtLmFwcC55bWwaiAEQDzKDARIEX3x8Xxo9EAkyOQoIEAgiBAoCZnMSCmZpbGVFeGlzdHMaIRAKGh0yGy5wbGF0Zm9ybS9hcHBsaWNhdGlvbnMueWFtbBo8EA0yOAoIEAwiBAoCZnMSCmZpbGVFeGlzdHMaIBAOGhwyGi5wbGF0Zm9ybS9hcHBsaWNhdGlvbnMueW1sKmkSBzxpbnB1dD4aAqkBIgQIAxAOIgQICRBaIgQICBBNIgQIARAAIgQIAhANIgQIBRA0IgUIDRCKASIECA8QeiIECAsQSiIECAoQWyIECAQQJyIECAYQNSIECAwQfSIFCA4QiwEiBAgHECQ= fs.fileExists("Cargo.toml") EhMIAhIPGg1mc19maWxlRXhpc3RzEggIARIECgJmcxoGCAESAgoAGgYIAhICGAEaBggDEgIYBSIsEAIyKAoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaEBADGgwyCkNhcmdvLnRvbWwqHhIHPGlucHV0PhoBHCIECAEQACIECAIQDSIECAMQDg== fs.fileExists("Gruntfile.js") || fs.fileExists("Gruntfile.coffee") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxIICAQSBAoCZnMSEwgFEg8aDWZzLmZpbGVFeGlzdHMSEAgHEgwaCmxvZ2ljYWxfb3IaBggDEgIYBRoGCAESAgoAGgYIAhICGAEaBggGEgIYBRoGCAQSAgoAGgYIBRICGAEaBggHEgIYASJuEAcyahIEX3x8XxouEAIyKgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaEhADGg4yDEdydW50ZmlsZS5qcxoyEAUyLgoIEAQiBAoCZnMSCmZpbGVFeGlzdHMaFhAGGhIyEEdydW50ZmlsZS5jb2ZmZWUqNhIHPGlucHV0PhoBQyIECAcQHiIECAEQACIECAIQDSIECAMQDiIECAQQISIECAUQLiIECAYQLw== fs.fileExists("Makefile") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIqEAIyJgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDhADGgoyCE1ha2VmaWxlKh4SBzxpbnB1dD4aARoiBAgBEAAiBAgCEA0iBAgDEA4= fs.fileExists("Pipfile") EhMIAhIPGg1mc19maWxlRXhpc3RzEggIARIECgJmcxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIpEAIyJQoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDRADGgkyB1BpcGZpbGUqHhIHPGlucHV0PhoBGSIECAEQACIECAIQDSIECAMQDg== +fs.fileExists("Procfile") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIqEAIyJgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDhADGgoyCFByb2NmaWxlKh4SBzxpbnB1dD4aARoiBAgBEAAiBAgCEA0iBAgDEA4= fs.fileExists("Rakefile") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIqEAIyJgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDhADGgoyCFJha2VmaWxlKh4SBzxpbnB1dD4aARoiBAgBEAAiBAgCEA0iBAgDEA4= fs.fileExists("Thorfile") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAESAgoAGgYIAhICGAEaBggDEgIYBSIqEAIyJgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDhADGgoyCFRob3JmaWxlKh4SBzxpbnB1dD4aARoiBAgCEA0iBAgDEA4iBAgBEAA= +fs.fileExists("app.json") && (fs.fileContains("app.json", '"buildpacks"') || fs.fileContains("app.json", '"addons"')) EhAIDBIMGgpsb2dpY2FsX29yEhEIDRINGgtsb2dpY2FsX2FuZBIICAESBAoCZnMSEwgCEg8aDWZzLmZpbGVFeGlzdHMSCAgEEgQKAmZzEhUIBRIRGg9mcy5maWxlQ29udGFpbnMSCAgIEgQKAmZzEhUICRIRGg9mcy5maWxlQ29udGFpbnMaBggHEgIYBRoGCAUSAhgBGgYICxICGAUaBggBEgIKABoGCAISAhgBGgYIBhICGAUaBggKEgIYBRoGCAwSAhgBGgYIAxICGAUaBggEEgIKABoGCAgSAgoAGgYICRICGAEaBggNEgIYASLFARANMsABEgRfJiZfGioQAjImCggQASIECgJmcxIKZmlsZUV4aXN0cxoOEAMaCjIIYXBwLmpzb24aiwEQDDKGARIEX3x8XxpAEAUyPAoIEAQiBAoCZnMSDGZpbGVDb250YWlucxoOEAYaCjIIYXBwLmpzb24aEhAHGg4yDCJidWlsZHBhY2tzIho8EAkyOAoIEAgiBAoCZnMSDGZpbGVDb250YWlucxoOEAoaCjIIYXBwLmpzb24aDhALGgoyCCJhZGRvbnMiKloSBzxpbnB1dD4aAXYiBAgLEGkiBAgNEBoiBAgCEA0iBAgHEDoiBAgMEEoiBAgIEE0iBAgKEF0iBAgBEAAiBAgFEC0iBAgGEC4iBAgJEFwiBAgDEA4iBAgEEB4= fs.fileExists("build.gradle") || fs.fileExists("build.gradle.kts") EggIARIECgJmcxITCAISDxoNZnNfZmlsZUV4aXN0cxIICAQSBAoCZnMSEwgFEg8aDWZzX2ZpbGVFeGlzdHMSEAgHEgwaCmxvZ2ljYWxfb3IaBggDEgIYBRoGCAESAgoAGgYIAhICGAEaBggGEgIYBRoGCAQSAgoAGgYIBRICGAEaBggHEgIYASJuEAcyahIEX3x8XxouEAIyKgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaEhADGg4yDGJ1aWxkLmdyYWRsZRoyEAUyLgoIEAQiBAoCZnMSCmZpbGVFeGlzdHMaFhAGGhIyEGJ1aWxkLmdyYWRsZS5rdHMqNhIHPGlucHV0PhoBQyIECAYQLyIECAcQHiIECAEQACIECAIQDSIECAMQDiIECAQQISIECAUQLg== fs.fileExists("build.sbt") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAESAgoAGgYIAhICGAEaBggDEgIYBSIrEAIyJwoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDxADGgsyCWJ1aWxkLnNidCoeEgc8aW5wdXQ+GgEbIgQIAhANIgQIAxAOIgQIARAA fs.fileExists("bun.lock") || fs.fileExists("bun.lockb") EggIBBIECgJmcxITCAUSDxoNZnNfZmlsZUV4aXN0cxIQCAcSDBoKbG9naWNhbF9vchIICAESBAoCZnMSEwgCEg8aDWZzX2ZpbGVFeGlzdHMaBggDEgIYBRoGCAESAgoAGgYIAhICGAEaBggGEgIYBRoGCAQSAgoAGgYIBRICGAEaBggHEgIYASJjEAcyXxIEX3x8XxoqEAIyJgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDhADGgoyCGJ1bi5sb2NrGisQBTInCggQBCIECgJmcxIKZmlsZUV4aXN0cxoPEAYaCzIJYnVuLmxvY2tiKjYSBzxpbnB1dD4aATgiBAgDEA4iBAgEEB0iBAgFECoiBAgGECsiBAgHEBoiBAgBEAAiBAgCEA0= @@ -170,6 +173,7 @@ fs.fileExists("fabfile.py") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAMSAhgF fs.fileExists("fresh.config.ts") || fs.depExists("js", "https://deno.land/x/fresh") EhIIBRIOGgxmcy5kZXBFeGlzdHMSEAgIEgwaCmxvZ2ljYWxfb3ISCAgBEgQKAmZzEhMIAhIPGg1mcy5maWxlRXhpc3RzEggIBBIECgJmcxoGCAMSAhgFGgYIARICCgAaBggCEgIYARoGCAYSAhgFGgYIBxICGAUaBggEEgIKABoGCAUSAhgBGgYICBICGAEigwEQCDJ/EgRffHxfGjEQAjItCggQASIECgJmcxIKZmlsZUV4aXN0cxoVEAMaETIPZnJlc2guY29uZmlnLnRzGkQQBTJACggQBCIECgJmcxIJZGVwRXhpc3RzGggQBhoEMgJqcxofEAcaGzIZaHR0cHM6Ly9kZW5vLmxhbmQveC9mcmVzaCo8Egc8aW5wdXQ+GgFUIgQIBhAxIgQIBxA3IgQICBAhIgQIARAAIgQIAhANIgQIAxAOIgQIBBAkIgQIBRAw fs.fileExists("gatsby-config.js") || fs.depExists("js", "gatsby") EhIIBRIOGgxmcy5kZXBFeGlzdHMSEAgIEgwaCmxvZ2ljYWxfb3ISCAgBEgQKAmZzEhMIAhIPGg1mcy5maWxlRXhpc3RzEggIBBIECgJmcxoGCAESAgoAGgYIAhICGAEaBggGEgIYBRoGCAcSAhgFGgYIBBICCgAaBggFEgIYARoGCAgSAhgBGgYIAxICGAUicRAIMm0SBF98fF8aMhACMi4KCBABIgQKAmZzEgpmaWxlRXhpc3RzGhYQAxoSMhBnYXRzYnktY29uZmlnLmpzGjEQBTItCggQBCIECgJmcxIJZGVwRXhpc3RzGggQBhoEMgJqcxoMEAcaCDIGZ2F0c2J5KjwSBzxpbnB1dD4aAUIiBAgBEAAiBAgCEA0iBAgDEA4iBAgEECUiBAgFEDEiBAgGEDIiBAgHEDgiBAgIECI= fs.fileExists("go.mod") || fs.fileExists("go.sum") EggIARIECgJmcxITCAISDxoNZnNfZmlsZUV4aXN0cxIICAQSBAoCZnMSEwgFEg8aDWZzX2ZpbGVFeGlzdHMSEAgHEgwaCmxvZ2ljYWxfb3IaBggEEgIKABoGCAUSAhgBGgYIBxICGAEaBggDEgIYBRoGCAESAgoAGgYIAhICGAEaBggGEgIYBSJeEAcyWhIEX3x8XxooEAIyJAoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDBADGggyBmdvLm1vZBooEAUyJAoIEAQiBAoCZnMSCmZpbGVFeGlzdHMaDBAGGggyBmdvLnN1bSo2Egc8aW5wdXQ+GgEzIgQIAhANIgQIAxAOIgQIBBAbIgQIBRAoIgQIBhApIgQIBxAYIgQIARAA +fs.fileExists("heroku.yml") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAISAhgBGgYIAxICGAUaBggBEgIKACIsEAIyKAoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaEBADGgwyCmhlcm9rdS55bWwqHhIHPGlucHV0PhoBHCIECAMQDiIECAEQACIECAIQDQ== fs.fileExists("hugo.toml") || fs.fileExists("hugo.yaml") || fs.fileExists("hugo.json") || fs.fileExists(".hugo_build.lock") EggICBIECgJmcxITCAkSDxoNZnMuZmlsZUV4aXN0cxITCA0SDxoNZnMuZmlsZUV4aXN0cxITCAUSDxoNZnMuZmlsZUV4aXN0cxIICAwSBAoCZnMSCAgEEgQKAmZzEhAIDxIMGgpsb2dpY2FsX29yEhAICxIMGgpsb2dpY2FsX29yEggIARIECgJmcxIQCAcSDBoKbG9naWNhbF9vchITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAMSAhgFGgYIBBICCgAaBggNEgIYARoGCAkSAhgBGgYIDBICCgAaBggLEgIYARoGCAgSAgoAGgYIAhICGAEaBggHEgIYARoGCAUSAhgBGgYIChICGAUaBggBEgIKABoGCA4SAhgFGgYIBhICGAUaBggPEgIYASLeARALMtkBEgRffHxfGmQQBzJgEgRffHxfGisQAjInCggQASIECgJmcxIKZmlsZUV4aXN0cxoPEAMaCzIJaHVnby50b21sGisQBTInCggQBCIECgJmcxIKZmlsZUV4aXN0cxoPEAYaCzIJaHVnby55YW1sGmsQDzJnEgRffHxfGisQCTInCggQCCIECgJmcxIKZmlsZUV4aXN0cxoPEAoaCzIJaHVnby5qc29uGjIQDTIuCggQDCIECgJmcxIKZmlsZUV4aXN0cxoWEA4aEjIQLmh1Z29fYnVpbGQubG9jaypmEgc8aW5wdXQ+GgF8IgQICBA8IgQIDhBoIgQIAhANIgQIAxAOIgQIChBKIgQIDRBnIgQICxA5IgQIARAAIgQICRBJIgQIBxAbIgQIBBAeIgQIBRArIgQIBhAsIgQIDBBaIgQIDxBX fs.fileExists("index.php") && fs.fileContains("index.php", "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxIICAQSBAoCZnMSFQgFEhEaD2ZzLmZpbGVDb250YWlucxIRCAgSDRoLbG9naWNhbF9hbmQaBggIEgIYARoGCAMSAhgFGgYIARICCgAaBggCEgIYARoGCAYSAhgFGgYIBxICGAUaBggEEgIKABoGCAUSAhgBIpcBEAgykgESBF8mJl8aKxACMicKCBABIgQKAmZzEgpmaWxlRXhpc3RzGg8QAxoLMglpbmRleC5waHAaXRAFMlkKCBAEIgQKAmZzEgxmaWxlQ29udGFpbnMaDxAGGgsyCWluZGV4LnBocBouEAcaKjIoZHJ1cGFsX2Jvb3RzdHJhcChEUlVQQUxfQk9PVFNUUkFQX0ZVTEwpOyo8Egc8aW5wdXQ+GgFnIgQIAxAOIgQIBBAeIgQIBRAtIgQIBhAuIgQIBxA7IgQICBAbIgQIARAAIgQIAhAN fs.fileExists("matomo.php") || (fs.fileExists("config/config.ini.php") && fs.fileContains("config/config.ini.php", "Matomo")) EggIBBIECgJmcxITCAUSDxoNZnMuZmlsZUV4aXN0cxIICAcSBAoCZnMSFQgIEhEaD2ZzLmZpbGVDb250YWlucxIRCAsSDRoLbG9naWNhbF9hbmQSEAgMEgwaCmxvZ2ljYWxfb3ISCAgBEgQKAmZzEhMIAhIPGg1mcy5maWxlRXhpc3RzGgYIAxICGAUaBggKEgIYBRoGCAgSAhgBGgYICxICGAEaBggCEgIYARoGCAYSAhgFGgYIARICCgAaBggEEgIKABoGCAUSAhgBGgYICRICGAUaBggHEgIKABoGCAwSAhgBIskBEAwyxAESBF98fF8aLBACMigKCBABIgQKAmZzEgpmaWxlRXhpc3RzGhAQAxoMMgptYXRvbW8ucGhwGo0BEAsyiAESBF8mJl8aNxAFMjMKCBAEIgQKAmZzEgpmaWxlRXhpc3RzGhsQBhoXMhVjb25maWcvY29uZmlnLmluaS5waHAaRxAIMkMKCBAHIgQKAmZzEgxmaWxlQ29udGFpbnMaGxAJGhcyFWNvbmZpZy9jb25maWcuaW5pLnBocBoMEAoaCDIGTWF0b21vKlQSBzxpbnB1dD4aAX4iBAgBEAAiBAgIEFkiBAgJEFoiBAgDEA4iBAgFEC0iBAgGEC4iBAgMEBwiBAgEECAiBAgCEA0iBAgHEEoiBAgKEHMiBAgLEEc= @@ -191,6 +195,7 @@ fs.fileExists("uv.lock") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAESAgoAGgY fs.fileExists("wp-config.php") || fs.depExists("php", "wordpress/wordpress") EhIIBRIOGgxmcy5kZXBFeGlzdHMSEAgIEgwaCmxvZ2ljYWxfb3ISCAgBEgQKAmZzEhMIAhIPGg1mcy5maWxlRXhpc3RzEggIBBIECgJmcxoGCAISAhgBGgYIBhICGAUaBggHEgIYBRoGCAQSAgoAGgYIBRICGAEaBggIEgIYARoGCAMSAhgFGgYIARICCgAifBAIMngSBF98fF8aLxACMisKCBABIgQKAmZzEgpmaWxlRXhpc3RzGhMQAxoPMg13cC1jb25maWcucGhwGj8QBTI7CggQBCIECgJmcxIJZGVwRXhpc3RzGgkQBhoFMgNwaHAaGRAHGhUyE3dvcmRwcmVzcy93b3JkcHJlc3MqPBIHPGlucHV0PhoBTSIECAIQDSIECAMQDiIECAQQIiIECAUQLiIECAYQLyIECAcQNiIECAgQHyIECAEQAA== fs.fileExists("yarn.lock") EggIARIECgJmcxITCAISDxoNZnNfZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIrEAIyJwoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDxADGgsyCXlhcm4ubG9jayoeEgc8aW5wdXQ+GgEbIgQIARAAIgQIAhANIgQIAxAO fs.glob("*.csproj").size() > 0 Eg8IBBILGglsaXN0X3NpemUSEwgFEg8aDWdyZWF0ZXJfaW50NjQSCAgBEgQKAmZzEg0IAhIJGgdmcy5nbG9iGgYIBhICGAIaBggFEgIYARoGCAMSAhgFGgYIARICCgAaCggCEgYyBAoCGAUaBggEEgIYAiJDEAUyPxIDXz5fGjAQBDIsCiQQAjIgCggQASIECgJmcxIEZ2xvYhoOEAMaCjIIKi5jc3Byb2oSBHNpemUaBhAGGgIYACowEgc8aW5wdXQ+GgEfIgQIAxAIIgQIBBAYIgQIBRAbIgQIBhAdIgQIARAAIgQIAhAH +fs.glob(".upsun/*.yaml").size() > 0 || fs.glob(".upsun/*.yml").size() > 0 Eg8IChILGglsaXN0X3NpemUSEwgFEg8aDWdyZWF0ZXJfaW50NjQSEwgLEg8aDWdyZWF0ZXJfaW50NjQSDwgEEgsaCWxpc3Rfc2l6ZRIQCA0SDBoKbG9naWNhbF9vchIICAESBAoCZnMSDQgCEgkaB2ZzLmdsb2ISCAgHEgQKAmZzEg0ICBIJGgdmcy5nbG9iGgYIBBICGAIaBggGEgIYAhoGCAcSAgoAGgYIAxICGAUaBggJEgIYBRoGCAsSAhgBGgYIBRICGAEaCggIEgYyBAoCGAUaBggKEgIYAhoGCAwSAhgCGgYIDRICGAEaBggBEgIKABoKCAISBjIECgIYBSKeARANMpkBEgRffHxfGkgQBTJEEgNfPl8aNRAEMjEKKRACMiUKCBABIgQKAmZzEgRnbG9iGhMQAxoPMg0udXBzdW4vKi55YW1sEgRzaXplGgYQBhoCGAAaRxALMkMSA18+Xxo0EAoyMAooEAgyJAoIEAciBAoCZnMSBGdsb2IaEhAJGg4yDC51cHN1bi8qLnltbBIEc2l6ZRoGEAwaAhgAKloSBzxpbnB1dD4aAUoiBAgKEEMiBAgLEEYiBAgCEAciBAgEEB0iBAgGECIiBAgNECQiBAgFECAiBAgIEC4iBAgMEEgiBAgBEAAiBAgDEAgiBAgHECciBAgJEC8= fs.glob("Gemfile*").size() > 0 EggIARIECgJmcxINCAISCRoHZnMuZ2xvYhIPCAQSCxoJbGlzdF9zaXplEhMIBRIPGg1ncmVhdGVyX2ludDY0GgYIBhICGAIaBggFEgIYARoGCAMSAhgFGgYIARICCgAaCggCEgYyBAoCGAUaBggEEgIYAiJDEAUyPxIDXz5fGjAQBDIsCiQQAjIgCggQASIECgJmcxIEZ2xvYhoOEAMaCjIIR2VtZmlsZSoSBHNpemUaBhAGGgIYACowEgc8aW5wdXQ+GgEfIgQIARAAIgQIAhAHIgQIAxAIIgQIBBAYIgQIBRAbIgQIBhAd fs.glob("docusaurus.config.*s").size() > 0 || fs.depExists("js", "@docusaurus/core") EhAICxIMGgpsb2dpY2FsX29yEggIARIECgJmcxINCAISCRoHZnMuZ2xvYhIPCAQSCxoJbGlzdF9zaXplEhMIBRIPGg1ncmVhdGVyX2ludDY0EggIBxIECgJmcxISCAgSDhoMZnMuZGVwRXhpc3RzGgYICBICGAEaBggDEgIYBRoKCAISBjIECgIYBRoGCAoSAhgFGgYIBxICCgAaBggFEgIYARoGCAkSAhgFGgYIARICCgAaBggEEgIYAhoGCAsSAhgBGgYIBhICGAIimQEQCzKUARIEX3x8XxpPEAUySxIDXz5fGjwQBDI4CjAQAjIsCggQASIECgJmcxIEZ2xvYhoaEAMaFjIUZG9jdXNhdXJ1cy5jb25maWcuKnMSBHNpemUaBhAGGgIYABo7EAgyNwoIEAciBAoCZnMSCWRlcEV4aXN0cxoIEAkaBDICanMaFhAKGhIyEEBkb2N1c2F1cnVzL2NvcmUqThIHPGlucHV0PhoBVSIECAcQLiIECAEQACIECAQQJCIECAsQKyIECAMQCCIECAUQJyIECAYQKSIECAgQOiIECAkQOyIECAoQQSIECAIQBw== fs.glob("gulpfile.*s").size() > 0 || fs.glob("Gulpfile.*s").size() > 0 Eg0IAhIJGgdmcy5nbG9iEhAIDRIMGgpsb2dpY2FsX29yEggIARIECgJmcxINCAgSCRoHZnMuZ2xvYhIPCAQSCxoJbGlzdF9zaXplEg8IChILGglsaXN0X3NpemUSEwgFEg8aDWdyZWF0ZXJfaW50NjQSCAgHEgQKAmZzEhMICxIPGg1ncmVhdGVyX2ludDY0GgYICRICGAUaBggHEgIKABoGCAMSAhgFGgYIBRICGAEaBggLEgIYARoGCA0SAhgBGgYIBBICGAIaCggCEgYyBAoCGAUaCggIEgYyBAoCGAUaBggKEgIYAhoGCAwSAhgCGgYIARICCgAaBggGEgIYAiKbARANMpYBEgRffHxfGkYQBTJCEgNfPl8aMxAEMi8KJxACMiMKCBABIgQKAmZzEgRnbG9iGhEQAxoNMgtndWxwZmlsZS4qcxIEc2l6ZRoGEAYaAhgAGkYQCzJCEgNfPl8aMxAKMi8KJxAIMiMKCBAHIgQKAmZzEgRnbG9iGhEQCRoNMgtHdWxwZmlsZS4qcxIEc2l6ZRoGEAwaAhgAKloSBzxpbnB1dD4aAUciBAgDEAgiBAgFEB4iBAgGECAiBAgIECwiBAgHECUiBAgMEEUiBAgNECIiBAgBEAAiBAgCEAciBAgEEBsiBAgJEC0iBAgKEEAiBAgLEEM= diff --git a/pkg/rules/analyze_testfs_test.go b/pkg/rules/analyze_testfs_test.go index da9d58a..be53a4a 100644 --- a/pkg/rules/analyze_testfs_test.go +++ b/pkg/rules/analyze_testfs_test.go @@ -91,6 +91,17 @@ var testFs = fstest.MapFS{ "jekyll-site/Gemfile": &fstest.MapFile{Data: []byte(`gem "jekyll", "~> 4.3"`)}, "jekyll-site/Gemfile.lock": &fstest.MapFile{Data: []byte(` jekyll (4.3.2)`)}, + // Heroku app with app.json containing buildpacks. + "heroku-app/Procfile": &fstest.MapFile{Data: []byte("web: gunicorn myapp:app")}, + "heroku-app/app.json": &fstest.MapFile{Data: []byte(`{"name": "myapp", "buildpacks": [{"url": "heroku/python"}]}`)}, + + // Heroku Docker-based app with heroku.yml. + "heroku-docker/heroku.yml": &fstest.MapFile{Data: []byte("build:\n docker:\n web: Dockerfile")}, + "heroku-docker/Procfile": &fstest.MapFile{Data: []byte("web: ./start.sh")}, + + // Upsun app. + "upsun-app/.upsun/config.yaml": &fstest.MapFile{Data: []byte("applications:\n myapp:\n type: 'php:8.3'")}, + // Additional directories to increase time taken. "deep/1/2/3/4/5/composer.json": &fstest.MapFile{Data: []byte("{}")}, "deep/a/b/c/d/e/package.json": &fstest.MapFile{Data: []byte("{}")}, @@ -180,6 +191,25 @@ func TestAnalyze_TestFS_ActualRules(t *testing.T) { Rules: []string{"npm-lockfile"}, Groups: []string{"js"}}, {Ruleset: "package_managers", Path: "python", Result: "uv", Rules: []string{"uv"}, Groups: []string{"python"}}, + + // Platform results. + {Ruleset: "platforms", Path: "configured-app", Result: "platformsh", + ReadFiles: []string{ + ".platform.app.yaml", ".platform.app.yml", + ".platform/applications.yaml", ".platform/applications.yml", + ".platform/routes.yaml", ".platform/routes.yml", + ".platform/services.yaml", ".platform/services.yml", + }, + Rules: []string{"platformsh"}, Groups: []string{"cloud"}}, + {Ruleset: "platforms", Path: "heroku-app", Result: "heroku", + ReadFiles: []string{".python-version", "Procfile", "app.json", "heroku.yml", "runtime.txt"}, + Rules: []string{"heroku-app-json"}, Groups: []string{"cloud"}}, + {Ruleset: "platforms", Path: "heroku-docker", Result: "heroku", + ReadFiles: []string{".python-version", "Procfile", "app.json", "heroku.yml", "runtime.txt"}, + Rules: []string{"heroku-yml"}, Groups: []string{"cloud"}}, + {Ruleset: "platforms", Path: "upsun-app", Result: "upsun", + ReadFiles: []string{".upsun/config.yaml", ".upsun/config.yml"}, + Rules: []string{"upsun"}, Groups: []string{"cloud"}}, }, reports) }