feat(lint): native config lint command for Flex and Fixed config#108
Open
pjcdawkins wants to merge 9 commits into
Open
feat(lint): native config lint command for Flex and Fixed config#108pjcdawkins wants to merge 9 commits into
pjcdawkins wants to merge 9 commits into
Conversation
Add internal/lint with the multi-error config linter ported from the ai-api repository (internal/linter, internal/schema, internal/registry, plus the .upsun config merge helpers). The linter validates merged Flex-style config against the embedded JSON schema and runs semantic checks (relationships, names, types, scripts, web, dependencies, routes), collecting all errors and warnings rather than stopping at the first. Changes from the source: - Inline the composable-image stable channel constant to drop the nix dependency. - Drop the AI-only file_modifications schema patch. - Use github.com/dlclark/regexp2/v2. Promote gojsonschema to a direct dependency and add mvdan.cc/sh/v3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the platformify-backed app:config-validate command (which delegated to the legacy PHP CLI) with a native Go command that runs the ported linter and reports all errors and warnings at once. - Add internal/lint/normalize.go: DetectStyle plus LintDir, which detect the configuration style from the directory layout (.upsun vs .platform) and lint the merged Flex configuration. Fixed-style linting is stubbed pending Phase 3. - Add commands/lint.go: the "lint" command (aliases "validate", "app:config-validate") accepting an optional path or stdin, with text and JSON output, exiting non-zero on errors. - Rename Lint to LintContent and add JSON tags to Issue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add linting for legacy Platform.sh configuration: .platform.app.yaml files and .platform/applications.yaml (list or map form), plus optional .platform/routes.yaml and .platform/services.yaml. CheckDir detects the style from the directory layout and normalizes Fixed-style config into the same Config the Flex path uses, so the semantic checks are shared. - Add the three Fixed-style JSON schemas (application, routes, services) copied from platformify, with per-file loaders and CheckSchemaScoped to attribute schema errors to their source file or app. - Gate composable-image and stack warnings to Flex in CheckTypes. - Guard against Flex-style keys appearing in a Fixed-style file. - Inject the application name from the map key when validating map-form applications.yaml, matching the canonical parser. - Rename the entrypoints to CheckContent and CheckDir, consistent with the Check* family, and satisfy the repository linters. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tooling to regenerate the embedded lint assets: - gen.go (build-tagged) fetches https://meta.upsun.com/images and transforms it into registry.json, mapping per-version status to supported/legacy and service/runtime. Regenerate with `go generate ./internal/lint/registry` or `make lint-assets`. - `make lint-assets` also refreshes the Flex and Fixed-style schemas from platformify. The meta.upsun.com schema is intentionally not used: it validates types via remote $refs (fetched at runtime) and duplicates the registry-based type check, so type and version validation stays in CheckTypes. - `make lint-assets-check` and a CI job fail when the committed assets are stale. Refresh the registry from meta.upsun.com and make the registry test robust to version drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the app:config-validate help metadata to document the optional path argument and the --format and --stdin options, and note the native lint command in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address findings from code review: - lint: only read piped stdin when it carries content; otherwise (a non-interactive shell or CI, where stdin is not a TTY but empty) fall back to linting the directory. Previously `lint` with no arguments errored with "empty content" in CI. Explicit --stdin still errors on empty input. - lint: make app:config-validate the primary command name (aliases lint, validate), matching the listing/help metadata and generated docs. - Fixed-style: reject a name set inside a map-form applications.yaml entry, matching the canonical parser and avoiding inconsistent app identity keying. - Add Result.Merge and use it instead of reallocating via Combine; drop the dead map[any]any branch in toStringMap; fix a misspelled identifier. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR replaces the legacy app:config-validate (aliases validate, lint) command that delegated to the PHP CLI with a native Go linter that validates both Flex (.upsun/*.yaml) and Fixed (.platform*.yaml) project configurations offline, reporting all issues in one run.
Changes:
- Added a native Cobra command for config linting with
--format text|jsonand optional path/stdin input handling. - Introduced
internal/lintwith schema validation + semantic checks (types/versions, scripts, web config, dependencies, routes, relationships, naming), plus Fixed-style normalization and embedded schemas. - Added tooling/CI to keep embedded registry + schemas in sync with upstream sources.
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Makefile | Adds lint-assets and lint-assets-check targets to refresh/verify embedded registry + schemas. |
| commands/root.go | Removes legacy Platformify validate command wiring and registers the new native lint command. |
| commands/list_models.go | Updates help/usage metadata for app:config-validate with path, --format, and --stdin. |
| commands/lint.go | Implements the new native app:config-validate command (aliases lint, validate) with text/json output. |
| internal/lint/linter.go | Adds CheckContent and wires schema + semantic checks for Flex-style content. |
| internal/lint/linter_test.go | Adds tests covering combined lints and common failure cases. |
| internal/lint/normalize.go | Detects Flex vs Fixed config layouts and dispatches to the appropriate loader/linter. |
| internal/lint/normalize_test.go | Tests style detection and directory linting behavior. |
| internal/lint/fixed.go | Loads Fixed-style config files, schema-validates them, normalizes into shared config shape, and runs semantic checks. |
| internal/lint/fixed_test.go | Tests Fixed-style loading/validation paths and edge cases. |
| internal/lint/merge.go | Merges `.upsun/*.yaml |
| internal/lint/merge_test.go | Tests merging behavior and error cases. |
| internal/lint/yaml.go | Adds YAML→Go decoding and JSON-schema validation helpers with scoped paths. |
| internal/lint/yaml_test.go | Tests YAML schema validation behavior on valid/invalid content. |
| internal/lint/result.go | Adds shared Result/Issue types and deterministic formatted output. |
| internal/lint/names.go | Adds application/service/worker name validation. |
| internal/lint/names_test.go | Tests name validation rules and error formatting. |
| internal/lint/types.go | Adds registry-backed type/version validation with Flex-only composable/stack warnings. |
| internal/lint/types_test.go | Tests type/version validation against a test registry. |
| internal/lint/relationships.go | Adds relationship target validation and “unused service” detection. |
| internal/lint/relationships_test.go | Tests relationship validation scenarios. |
| internal/lint/scripts.go | Adds POSIX shell syntax validation for hooks/commands/cron scripts + start-command warnings. |
| internal/lint/scripts_test.go | Tests script parsing failures and warning behavior. |
| internal/lint/web.go | Adds web location key/root path checks and rule regex validation. |
| internal/lint/web_test.go | Comprehensive tests for web linting rules and error formatting. |
| internal/lint/routes.go | Adds basic route upstream target/protocol validation. |
| internal/lint/routes_test.go | Tests route linting scenarios and expected messages. |
| internal/lint/dependencies.go | Adds dependency section validation (type and empty values). |
| internal/lint/dependencies_test.go | Tests dependency validation including complex PHP dependency shapes. |
| internal/lint/config_schema.go | Defines shared decoded config model used by semantic checks. |
| internal/lint/schema/schema.go | Embeds and loads the Flex JSON schema with sync.Once caching. |
| internal/lint/schema/schema_test.go | Smoke test that the embedded Flex schema validates a basic config. |
| internal/lint/schema/fixed.go | Embeds and loads Fixed-style schemas (application/routes/services). |
| internal/lint/schema/platformsh.application.json | Embedded Fixed application JSON schema. |
| internal/lint/schema/platformsh.routes.json | Embedded Fixed routes JSON schema. |
| internal/lint/schema/platformsh.services.json | Embedded Fixed services JSON schema. |
| internal/lint/registry/registry.go | Embeds registry data and provides parsing + normalization (clean()). |
| internal/lint/registry/registry_test.go | Smoke test that the embedded registry parses and includes expected entries. |
| internal/lint/registry/model.go | Defines registry model types and JSON unmarshalling behavior for versions. |
| internal/lint/registry/model_test.go | Tests registry model parsing helpers and template-friendly mapping. |
| internal/lint/registry/gen.go | Generator that fetches meta.upsun.com/images and writes registry.json. |
| internal/lint/registry/registry.json | Embedded minimized registry snapshot consumed by the linter. |
| internal/lint/testdata/registry.json | Test registry fixture used by type-check tests. |
| go.mod | Adds new dependencies used by the native linter (schema, regex, shell parser). |
| go.sum | Updates module checksums for the new/updated dependencies. |
| .github/workflows/ci.yml | Adds a CI job to fail when embedded lint assets are stale. |
| CLAUDE.md | Updates repo documentation to include the new native lint command. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The "available targets: ..." message in CheckRoutes was built by ranging over a map, so its order varied between runs. Sort it before joining. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make config-style detection match how a project actually deploys, and drive the directory names from the CLI's own config so vendor/white-label builds work. - Resolve the project root by walking up to the nearest enclosing .git (falling back to the given path), so lint can run from any subdirectory. The nearest .git is used, not the topmost, so a stray repository higher up the tree (e.g. a dotfiles repo, or /tmp) cannot hijack the result. - Detect Flex vs Fixed from the vendor's conventions (project_config_flavor, project_config_dir, app_config_file) plus the files present: Flex wins when present, else Fixed, else the native format. A first-party build (.upsun or .platform) also recognizes the other first-party format as a migration case; white-label builds use only their own names. - Anchor Fixed detection at the root (a config directory or a top-level app file). Nested per-app files are still collected once a project is confirmed, but a stray fixture no longer turns an unrelated repo into a project. - Warn about nested copies of any known config directory (.upsun, .platform, or the configured dir); the platform only reads them at the project root. - Make the duplicate application-name error name both source files. - Accept .yml as well as .yaml for Fixed routes/services/applications files. - Add app_config_file to the Go config schema (it was already in the YAML). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Print the validated directory on its own line every run
("Validating configuration in directory: <path>", path in cyan).
- Colour only the "Linter errors:" / "Linter warnings:" headings
(bold red / bold yellow); the issue lines use the default colour so
they stay readable.
- Keep the green check mark but leave "The configuration is valid."
in the default colour.
- Capitalize the first letter of operational errors for display (the Go
error strings stay lowercase per convention), so "no configuration
found" reads as "No configuration found".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines
+25
to
+35
| // Group all scripts for shell syntax checking. | ||
| scripts[keyPrefix+"hooks.build"] = app.Hooks.Build | ||
| scripts[keyPrefix+"hooks.deploy"] = app.Hooks.Deploy | ||
| scripts[keyPrefix+"hooks.post_deploy"] = app.Hooks.PostDeploy | ||
| scripts[keyPrefix+"web.commands.start"] = app.Web.Commands.Start | ||
| scripts[keyPrefix+"web.commands.post_start"] = app.Web.Commands.PostStart | ||
| for cronName, cron := range app.Crons { | ||
| cronPrefix := keyPrefix + "crons." + cronName + "." | ||
| scripts[cronPrefix+"start"] = cron.Commands.Start | ||
| scripts[cronPrefix+"stop"] = cron.Commands.Stop | ||
| } |
Comment on lines
+79
to
+98
| func mockSchema() *gojsonschema.Schema { | ||
| schema, _ := gojsonschema.NewSchema(gojsonschema.NewStringLoader(` | ||
| { | ||
| "$schema": "http://json-schema.org/draft-07/schema#", | ||
| "type": "object", | ||
| "properties": { | ||
| "key": { | ||
| "type": "string" | ||
| }, | ||
| "list": { | ||
| "type": "array", | ||
| "items": { | ||
| "type": "string" | ||
| } | ||
| } | ||
| }, | ||
| "required": ["key"] | ||
| }`)) | ||
| return schema | ||
| } |
Comment on lines
+141
to
+143
| lint-assets: ## Refresh the embedded lint registry and schemas from upstream | ||
| cd internal/lint/registry && GOEXPERIMENT=jsonv2 go run gen.go | ||
| curl -sfSL $(PLATFORMIFY_SCHEMA_URL)/upsun.json -o internal/lint/schema/upsun-config-schema.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the
app:config-validatecommand (aliasesvalidate,lint), which used the platformify library's schema validator and reported a single error at a time, with a newlintcommand that reports all errors and warnings at once. It runs a JSON-schema check plus semantic checks (relationships, names, types/versions, scripts, web config, dependencies, routes).It works for both configuration styles:
.upsun/*.yaml, merged..platform.app.yamlfiles and/or.platform/applications.yaml(list or map form), plus optional.platform/routes.yamland.platform/services.yaml. The.ymlextension is accepted as well as.yaml.About 8600 lines of this PR are the new
internal/lintpackage - almost entirely copied from the internal AI API project where it has been used in production for ~ 9 months.In the AI API it only supported "Flex" validation, so the "Fixed" configuration is now normalized into the same shape, so the further checks are shared across both styles.
Project root and style detection
Detection is offline (no API calls) and based on the running CLI's own config plus the files present:
.gitdirectory (falling back to the given path), so the command can run from any subdirectory. The nearest, not topmost,.gitis used so a stray repository higher up the tree cannot hijack the result.project_config_flavor,project_config_dir,app_config_file), so vendor/white-label builds work. The style is chosen as: Flex when present, else Fixed, else the build's native format. A first-party build (.upsunor.platform) also recognizes the other first-party format as a migration case; white-label builds use only their own names.Details
commands/lint.go: the command, taking an optional[path]or piped stdin, with--format text|json. Exits non-zero on errors. Piped stdin is only consumed when it carries content, solintwith no arguments in a non-interactive shell or CI lints the current directory instead of erroring.internal/lint: the pure linter (CheckDir,CheckContent), Fixed-style loaders, and the embedded Flex/Fixed JSON schemas.internal/lint/registry: the image registry.gen.gotransformsmeta.upsun.com/imagesinto the embeddedregistry.json.make lint-assetsrefreshes the embedded registry and schemas;make lint-assets-checkand a CI job fail when they are stale.Warnings and messages
.platformor.upsun) is warned about, since the platform only reads it at the root.Out of scope
Folding the Platformify repository into this one is a separate follow-up; only the three Fixed-style schemas were copied in for now. The Platformify dependency is still used by the
initcommand.The meta.upsun.com schema is intentionally not used for validation: it resolves type and version via remote
$refs (fetched at runtime, which would break offline use) and duplicates the registry-based type check.🤖 Generated with Claude Code