Conversation
…and raw file URLs
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65 +/- ##
==========================================
- Coverage 92.60% 91.56% -1.05%
==========================================
Files 32 33 +1
Lines 2503 2678 +175
==========================================
+ Hits 2318 2452 +134
- Misses 115 147 +32
- Partials 70 79 +9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds remote import support to the raid profile add command so profiles can be registered from either local files, git repository URLs, or direct raw file URLs—improving profile sharing workflows across teams.
Changes:
- Added URL detection + fetch/clone flow for
raid profile add, including profile discovery in cloned repos and downloading raw profile files. - Introduced
sys.CopyFileutility (with tests) to persist fetched profile files locally before registration. - Updated CLI docs and added extensive tests covering URL-based profile imports and failure modes.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/cmd/profile/fetch.go | New URL import implementation (detect git vs raw file, clone/download, discover profiles, register them). |
| src/cmd/profile/add.go | Routes URL arguments to the new fetch/import path. |
| src/cmd/profile/fetch_test.go | New unit + subprocess tests for URL import paths and error handling. |
| src/cmd/profile/profile_test.go | Extends mocking harness to include new injectable fetch/git/http funcs. |
| src/internal/sys/system.go | Adds CopyFile helper used by URL import to persist files. |
| src/internal/sys/system_test.go | Adds tests for CopyFile. |
| site/docs/whats-new.mdx | Documents new URL-based profile add capability. |
| site/docs/features/profiles.mdx | Adds feature documentation and examples for URL imports. |
| site/docs/usage/profile.mdx | Updates usage docs with URL-based profile add examples and behavior. |
| llms.txt | Updates command summary to mention URL-based profile add support. |
|
|
||
| ## 0.7.0 — upcoming | ||
|
|
||
| **Add profiles from a URL.** `raid profile add` now accepts a git repo URL or a direct file URL in addition to a local path. Pass a GitHub (or any git host) URL and raid shallow-clones the repo, finds `*.raid.yaml` files at the root, and saves them to `~/<name>.raid.yaml`. Pass a raw HTTPS URL ending in `.yaml`/`.yml`/`.json` and raid downloads and registers it directly. Ambiguous HTTPS URLs are probed with `git ls-remote` to determine the right strategy automatically. |
There was a problem hiding this comment.
Release notes mention importing *.raid.yaml files from a repo, but the code also supports *.raid.yml and profile.json. Consider updating this note to reflect the full supported set (or restricting the implementation).
| **Add profiles from a URL.** `raid profile add` now accepts a git repo URL or a direct file URL in addition to a local path. Pass a GitHub (or any git host) URL and raid shallow-clones the repo, finds `*.raid.yaml` files at the root, and saves them to `~/<name>.raid.yaml`. Pass a raw HTTPS URL ending in `.yaml`/`.yml`/`.json` and raid downloads and registers it directly. Ambiguous HTTPS URLs are probed with `git ls-remote` to determine the right strategy automatically. | |
| **Add profiles from a URL.** `raid profile add` now accepts a git repo URL or a direct file URL in addition to a local path. Pass a GitHub (or any git host) URL and raid shallow-clones the repo, finds `*.raid.yaml`, `*.raid.yml`, or `profile.json` files at the root, and saves them to `~/<name>.raid.yaml`. Pass a raw HTTPS URL ending in `.yaml`/`.yml`/`.json` and raid downloads and registers it directly. Ambiguous HTTPS URLs are probed with `git ls-remote` to determine the right strategy automatically. |
| home := getHomeDir() | ||
| var toRegister []pro.Profile | ||
| var destPaths []string | ||
| for _, q := range queued { | ||
| destPath := filepath.Join(home, q.p.Name+".raid.yaml") | ||
| if err := sys.CopyFile(q.srcPath, destPath); err != nil { | ||
| fmt.Printf("Failed to save profile '%s': %v\n", q.p.Name, err) | ||
| continue |
There was a problem hiding this comment.
processProfileFiles uses q.p.Name (from remote profile content) to build destPath without validating/sanitizing it. A crafted name containing path separators could write outside the home directory (path traversal) or create unexpected paths. Validate the profile name with sys.ValidateFileName (and/or enforce a safe filename mapping) before using it in filepath.Join and skip invalid names with a clear message.
| for _, p := range profiles { | ||
| if proContains(p.Name) { | ||
| existingNames = append(existingNames, p.Name) | ||
| continue | ||
| } | ||
| queued = append(queued, pending{p: p, srcPath: srcPath}) | ||
| } |
There was a problem hiding this comment.
queued can contain multiple entries with the same profile name (e.g., duplicates across files, or within multi-doc YAML). Later, destPath := <home>/<name>.raid.yaml will be the same and the copies will overwrite each other before registration. Deduplicate queued profiles by name (and/or detect conflicts and skip with an explicit message) before copying/writing.
| // CopyFile copies the file at src to dest, creating parent directories as needed. | ||
| func CopyFile(src, dest string) error { | ||
| data, err := os.ReadFile(src) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { | ||
| return err | ||
| } | ||
| return os.WriteFile(dest, data, 0644) | ||
| } |
There was a problem hiding this comment.
CopyFile does not call ExpandPath on src/dest (unlike CreateFile and FileExists) and returns raw errors without context. Consider expanding both paths and wrapping errors with enough context to debug failures. Also, reading the whole file into memory can be avoided by streaming copy (open src/dest and io.Copy).
| ```bash | ||
| # Shallow-clone a repo and import any *.raid.yaml files found at the root | ||
| raid profile add https://github.com/my-org/raid-profiles | ||
|
|
||
| # Download a single profile file directly | ||
| raid profile add https://raw.githubusercontent.com/my-org/repo/main/team.raid.yaml | ||
|
|
||
| # Register a local file | ||
| raid profile add ./team.raid.yaml | ||
| ``` |
There was a problem hiding this comment.
This description says repo import finds *.raid.yaml files, but the implementation also supports *.raid.yml and profile.json. Please align documentation and behavior to avoid surprises.
| u, _ := url.Parse(rawURL) | ||
| ext := strings.ToLower(filepath.Ext(u.Path)) | ||
| if ext == "" { | ||
| ext = ".yaml" | ||
| } |
There was a problem hiding this comment.
url.Parse(rawURL) errors are ignored here; if parsing fails and returns a nil URL, using u.Path will panic. Handle the parse error and return a user-facing "invalid URL" error/exit code instead of continuing.
| httpGetFunc = func(rawURL string) ([]byte, error) { | ||
| resp, err := http.Get(rawURL) //nolint:gosec | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() | ||
| if resp.StatusCode != http.StatusOK { | ||
| return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, rawURL) | ||
| } | ||
| return io.ReadAll(resp.Body) | ||
| } |
There was a problem hiding this comment.
httpGetFunc uses http.Get with no timeout and then io.ReadAll with no size limit. A slow or very large response can hang the command or exhaust memory. Use an http.Client with a reasonable timeout and read via an io.LimitReader (and surface a helpful error when the limit is exceeded).
| entries, _ := os.ReadDir(dir) | ||
| for _, e := range entries { | ||
| if e.IsDir() { | ||
| continue | ||
| } | ||
| name := e.Name() | ||
| lower := strings.ToLower(name) | ||
| ext := filepath.Ext(lower) | ||
| stem := strings.TrimSuffix(lower, ext) | ||
| if (ext == ".yaml" || ext == ".yml") && strings.HasSuffix(stem, ".raid") { | ||
| add(name) | ||
| } |
There was a problem hiding this comment.
os.ReadDir(dir) errors are dropped, which can lead to silently missing profile files and returning confusing "No profile files found" results. Handle the error and either return it (so the caller can fail) or print a warning and proceed with only the explicitly-checked filenames.
| entries, _ := os.ReadDir(dir) | |
| for _, e := range entries { | |
| if e.IsDir() { | |
| continue | |
| } | |
| name := e.Name() | |
| lower := strings.ToLower(name) | |
| ext := filepath.Ext(lower) | |
| stem := strings.TrimSuffix(lower, ext) | |
| if (ext == ".yaml" || ext == ".yml") && strings.HasSuffix(stem, ".raid") { | |
| add(name) | |
| } | |
| entries, err := os.ReadDir(dir) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "warning: failed to read profile directory %q: %v\n", dir, err) | |
| } else { | |
| for _, e := range entries { | |
| if e.IsDir() { | |
| continue | |
| } | |
| name := e.Name() | |
| lower := strings.ToLower(name) | |
| ext := filepath.Ext(lower) | |
| stem := strings.TrimSuffix(lower, ext) | |
| if (ext == ".yaml" || ext == ".yml") && strings.HasSuffix(stem, ".raid") { | |
| add(name) | |
| } | |
| } |
| Add a profile from a git repository (raid shallow-clones it and looks for `*.raid.yaml` files at the repo root): | ||
|
|
||
| ```bash | ||
| raid profile add https://github.com/my-org/raid-profiles | ||
| raid profile add git@github.com:my-org/raid-profiles.git | ||
| ``` |
There was a problem hiding this comment.
Docs say the repo import looks for *.raid.yaml files only, but findProfileFilesInDir also imports *.raid.yml and profile.json. Either update the docs to match the actual detection behavior or narrow the implementation to what is documented.
…es and improve error handling
This pull request adds support for importing profiles from remote sources in addition to local files. Now,
raid profile addcan accept a git repository URL or a direct file URL, automatically detecting the type and handling the download or clone process. The documentation and tests are updated to reflect and validate this new functionality.New profile import functionality:
src/cmd/profile/fetch.go: Added logic to detect if the argument is a URL, distinguish between git and raw file URLs, clone or download as appropriate, and register found profiles. Includes robust file detection, error handling, and ensures profiles are saved to the user's home directory.src/cmd/profile/add.go: Updated to delegate to the new URL-handling logic when a URL is provided.Documentation updates:
site/docs/whats-new.mdx,site/docs/features/profiles.mdx,site/docs/usage/profile.mdx,llms.txt: Updated to document the new ability to add profiles from git repositories and direct file URLs, including usage examples and behavioral details. [1] [2] [3] [4]Internal utilities and testing:
src/internal/sys/system.go,src/internal/sys/system_test.go: Added and tested a newCopyFileutility to support copying profile files to their final destination. [1] [2]src/cmd/profile/profile_test.go: Extended test harness to mock the new injectable functions for git and HTTP operations, ensuring testability of the new import logic. [1] [2] [3]These changes make it much easier to share and reuse profile configurations across teams and environments.…and raw file URLs
Closes #23