Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pkg/cli/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func fetchRemoteWorkflow(spec *WorkflowSpec, verbose bool) (*FetchedWorkflow, er
}

// Resolve the ref to a commit SHA for source tracking
commitSHA, err := parser.ResolveRefToSHA(owner, repo, ref)
commitSHA, err := parser.ResolveRefToSHAForHost(owner, repo, ref, spec.Host)
if err != nil {
remoteWorkflowLog.Printf("Failed to resolve ref to SHA: %v", err)
// Continue without SHA - we can still fetch the content
Expand All @@ -96,7 +96,7 @@ func fetchRemoteWorkflow(spec *WorkflowSpec, verbose bool) (*FetchedWorkflow, er
}

// Download the workflow file from GitHub
content, err := parser.DownloadFileFromGitHub(owner, repo, spec.WorkflowPath, ref)
content, err := parser.DownloadFileFromGitHubForHost(owner, repo, spec.WorkflowPath, ref, spec.Host)
if err != nil {
// Try with common workflow directory prefixes if the direct path fails.
// This handles short workflow names without path separators (e.g. "my-workflow.md").
Expand All @@ -107,7 +107,7 @@ func fetchRemoteWorkflow(spec *WorkflowSpec, verbose bool) (*FetchedWorkflow, er
altPath += ".md"
}
remoteWorkflowLog.Printf("Direct path failed, trying: %s", altPath)
if altContent, altErr := parser.DownloadFileFromGitHub(owner, repo, altPath, ref); altErr == nil {
if altContent, altErr := parser.DownloadFileFromGitHubForHost(owner, repo, altPath, ref, spec.Host); altErr == nil {
return &FetchedWorkflow{
Content: altContent,
CommitSHA: commitSHA,
Expand Down
32 changes: 26 additions & 6 deletions pkg/cli/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type WorkflowSpec struct {
WorkflowPath string // e.g., "workflows/workflow-name.md"
WorkflowName string // e.g., "workflow-name"
IsWildcard bool // true if this is a wildcard spec (e.g., "owner/repo/*")
Host string // explicit hostname from URL (e.g., "github.com", "myorg.ghe.com"); empty = use configured GH_HOST
}

// isLocalWorkflowPath checks if a path refers to a local filesystem workflow.
Expand Down Expand Up @@ -148,19 +149,21 @@ func parseRepoSpec(repoSpec string) (*RepoSpec, error) {
// - https://raw.githubusercontent.com/owner/repo/refs/heads/branch/path/to/workflow.md
// - https://raw.githubusercontent.com/owner/repo/COMMIT_SHA/path/to/workflow.md
// - https://raw.githubusercontent.com/owner/repo/refs/tags/tag/path/to/workflow.md
// - https://myorg.ghe.com/owner/repo/blob/branch/path/to/workflow.md (GHE)
func parseGitHubURL(spec string) (*WorkflowSpec, error) {
specLog.Printf("Parsing GitHub URL: %s", spec)
// First validate that this is a GitHub URL (github.com or raw.githubusercontent.com)
parsedURL, err := url.Parse(spec)
if err != nil {
specLog.Printf("Failed to parse URL: %v", err)
return nil, fmt.Errorf("invalid URL: %w", err)
}

// Must be a GitHub URL
if parsedURL.Host != "github.com" && parsedURL.Host != "raw.githubusercontent.com" {
specLog.Printf("Invalid host: %s", parsedURL.Host)
return nil, errors.New("URL must be from github.com or raw.githubusercontent.com")
if parsedURL.Host == "" {
return nil, fmt.Errorf("URL must include a host: %s", spec)
}

if !isGitHubHost(parsedURL.Host) {
return nil, fmt.Errorf("URL must be from github.com or a GitHub Enterprise host (*.ghe.com), got %q", parsedURL.Host)
}

owner, repo, ref, filePath, err := parser.ParseRepoFileURL(spec)
Expand All @@ -169,7 +172,7 @@ func parseGitHubURL(spec string) (*WorkflowSpec, error) {
return nil, err
}

specLog.Printf("Parsed GitHub URL: owner=%s, repo=%s, ref=%s, path=%s", owner, repo, ref, filePath)
specLog.Printf("Parsed GitHub URL: owner=%s, repo=%s, ref=%s, path=%s, host=%s", owner, repo, ref, filePath, parsedURL.Host)

// Ensure the file path ends with .md
if !strings.HasSuffix(filePath, ".md") {
Expand All @@ -181,20 +184,37 @@ func parseGitHubURL(spec string) (*WorkflowSpec, error) {
return nil, fmt.Errorf("invalid GitHub URL: '%s/%s' does not look like a valid GitHub repository", owner, repo)
}

// For raw.githubusercontent.com content, the API host is github.com.
// For all other hosts (github.com, GHE), use the URL's host as-is.
host := parsedURL.Host
if host == "raw.githubusercontent.com" {
host = "github.com"
}

Comment on lines 153 to +193
return &WorkflowSpec{
RepoSpec: RepoSpec{
RepoSlug: fmt.Sprintf("%s/%s", owner, repo),
Version: ref,
},
WorkflowPath: filePath,
WorkflowName: normalizeWorkflowID(filePath),
Host: host,
}, nil
}

// parseWorkflowSpec parses a workflow specification in the new format
// Format: owner/repo/workflows/workflow-name[@version] or owner/repo/workflow-name[@version]
// Also supports full GitHub URLs like https://github.com/owner/repo/blob/branch/path/to/workflow.md
// Also supports local paths like ./workflows/workflow-name.md

// isGitHubHost returns true if the given host is a recognized GitHub or GitHub Enterprise host:
// github.com, raw.githubusercontent.com, or any *.ghe.com host.
func isGitHubHost(host string) bool {
return host == "github.com" ||
host == "raw.githubusercontent.com" ||
strings.HasSuffix(host, ".ghe.com") ||
strings.HasSuffix(host, ".github.com")
}
func parseWorkflowSpec(spec string) (*WorkflowSpec, error) {
specLog.Printf("Parsing workflow spec: %q", spec)

Expand Down
19 changes: 17 additions & 2 deletions pkg/cli/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func TestParseWorkflowSpec(t *testing.T) {
wantWorkflowPath string
wantWorkflowName string
wantVersion string
wantHost string
wantErr bool
errContains string
}{
Expand All @@ -142,6 +143,7 @@ func TestParseWorkflowSpec(t *testing.T) {
wantWorkflowPath: "workflows/release-issue-linker.md",
wantWorkflowName: "release-issue-linker",
wantVersion: "main",
wantHost: "github.com",
wantErr: false,
},
{
Expand Down Expand Up @@ -181,10 +183,20 @@ func TestParseWorkflowSpec(t *testing.T) {
wantErr: false,
},
{
name: "GitHub URL - invalid domain",
name: "GitHub URL - GHE.com instance",
spec: "https://myorg.ghe.com/owner/repo/blob/main/workflows/test.md",
wantRepo: "owner/repo",
wantWorkflowPath: "workflows/test.md",
wantWorkflowName: "test",
wantVersion: "main",
wantHost: "myorg.ghe.com",
wantErr: false,
},
{
name: "GitHub URL - non-github.com host is rejected (e.g. gitlab.com)",
spec: "https://gitlab.com/owner/repo/blob/main/workflows/test.md",
wantErr: true,
errContains: "must be from github.com",
errContains: "github.com",
},
{
name: "GitHub URL - missing file extension",
Expand Down Expand Up @@ -328,6 +340,9 @@ func TestParseWorkflowSpec(t *testing.T) {
if spec.Version != tt.wantVersion {
t.Errorf("parseWorkflowSpec() version = %q, want %q", spec.Version, tt.wantVersion)
}
if tt.wantHost != "" && spec.Host != tt.wantHost {
t.Errorf("parseWorkflowSpec() host = %q, want %q", spec.Host, tt.wantHost)
}
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/import_remote_nested_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ func TestResolveRemoteSymlinksPathConstruction(t *testing.T) {
// GitHub API access, which is tested in integration tests.

t.Run("single component path returns error", func(t *testing.T) {
_, err := resolveRemoteSymlinks("owner", "repo", "file.md", "main")
_, err := resolveRemoteSymlinks(nil, "owner", "repo", "file.md", "main")
assert.Error(t, err, "Single component path has no directories to resolve")
})

Expand Down
Loading
Loading