Skip to content

Commit 2c1f5b2

Browse files
Merge pull request cli#13264 from SamMorrowDrums/sammorrowdrums/skill-ghec-data-residency
feat(skills): support GHEC with data residency hosts
2 parents 96b9af3 + 63262dc commit 2c1f5b2

11 files changed

Lines changed: 52 additions & 19 deletions

File tree

internal/skills/installer/installer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func Install(opts *Options) (*Result, error) {
7676
return nil, fmt.Errorf("failed to install skill %q: %w", skill.InstallName(), err)
7777
}
7878
var warnings []string
79-
if err := lockfile.RecordInstall(skill.InstallName(), opts.Owner, opts.Repo, skill.Path+"/SKILL.md", skill.TreeSHA, opts.PinnedRef); err != nil {
79+
if err := lockfile.RecordInstall(opts.Host, skill.InstallName(), opts.Owner, opts.Repo, skill.Path+"/SKILL.md", skill.TreeSHA, opts.PinnedRef); err != nil {
8080
warnings = append(warnings, fmt.Sprintf("could not record install for %s: %v", skill.InstallName(), err))
8181
}
8282
return &Result{Installed: []string{skill.InstallName()}, Dir: targetDir, Warnings: warnings}, nil
@@ -129,7 +129,7 @@ func Install(opts *Options) (*Result, error) {
129129
}
130130
installed = append(installed, r.name)
131131
skill := opts.Skills[i]
132-
if err := lockfile.RecordInstall(skill.InstallName(), opts.Owner, opts.Repo, skill.Path+"/SKILL.md", skill.TreeSHA, opts.PinnedRef); err != nil {
132+
if err := lockfile.RecordInstall(opts.Host, skill.InstallName(), opts.Owner, opts.Repo, skill.Path+"/SKILL.md", skill.TreeSHA, opts.PinnedRef); err != nil {
133133
warnings = append(warnings, fmt.Sprintf("could not record install for %s: %v", skill.InstallName(), err))
134134
}
135135
}

internal/skills/lockfile/lockfile.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/cli/cli/v2/internal/flock"
13+
"github.com/cli/cli/v2/internal/ghinstance"
1314
)
1415

1516
const (
@@ -93,7 +94,7 @@ func writeTo(f *os.File, lf *file) error {
9394
// RecordInstall adds or updates a skill entry in the lock file.
9495
// It uses a file-based lock to prevent concurrent read-modify-write races
9596
// when multiple install processes run simultaneously.
96-
func RecordInstall(skillName, owner, repo, skillPath, treeSHA, pinnedRef string) error {
97+
func RecordInstall(host, skillName, owner, repo, skillPath, treeSHA, pinnedRef string) error {
9798
lockPath, err := lockfilePath()
9899
if err != nil {
99100
return err
@@ -124,7 +125,7 @@ func RecordInstall(skillName, owner, repo, skillPath, treeSHA, pinnedRef string)
124125
f.Skills[skillName] = entry{
125126
Source: owner + "/" + repo,
126127
SourceType: "github",
127-
SourceURL: "https://github.com/" + owner + "/" + repo + ".git",
128+
SourceURL: ghinstance.HostPrefix(host) + owner + "/" + repo + ".git",
128129
SkillPath: skillPath,
129130
SkillFolderHash: treeSHA,
130131
InstalledAt: installedAt,

internal/skills/lockfile/lockfile_test.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func TestRecordInstall(t *testing.T) {
2424
tests := []struct {
2525
name string
2626
setup func(t *testing.T)
27+
host string
2728
skill string
2829
owner string
2930
repo string
@@ -35,6 +36,7 @@ func TestRecordInstall(t *testing.T) {
3536
}{
3637
{
3738
name: "fresh install creates lockfile",
39+
host: "github.com",
3840
skill: "code-review",
3941
owner: "monalisa",
4042
repo: "octocat-skills",
@@ -55,8 +57,25 @@ func TestRecordInstall(t *testing.T) {
5557
assert.Empty(t, e.PinnedRef)
5658
},
5759
},
60+
{
61+
name: "tenancy host uses correct URL",
62+
host: "mycompany.ghe.com",
63+
skill: "code-review",
64+
owner: "monalisa",
65+
repo: "octocat-skills",
66+
skillPath: "skills/code-review/SKILL.md",
67+
treeSHA: "abc123",
68+
verify: func(t *testing.T, lockPath string) {
69+
t.Helper()
70+
f := readTestLockfile(t, lockPath)
71+
require.Contains(t, f.Skills, "code-review")
72+
e := f.Skills["code-review"]
73+
assert.Equal(t, "https://mycompany.ghe.com/monalisa/octocat-skills.git", e.SourceURL)
74+
},
75+
},
5876
{
5977
name: "install with pinned ref",
78+
host: "github.com",
6079
skill: "pr-summary",
6180
owner: "hubot",
6281
repo: "skills-repo",
@@ -73,8 +92,9 @@ func TestRecordInstall(t *testing.T) {
7392
name: "multiple skills coexist",
7493
setup: func(t *testing.T) {
7594
t.Helper()
76-
require.NoError(t, RecordInstall("code-review", "monalisa", "octocat-skills", "skills/code-review/SKILL.md", "sha1", ""))
95+
require.NoError(t, RecordInstall("github.com", "code-review", "monalisa", "octocat-skills", "skills/code-review/SKILL.md", "sha1", ""))
7796
},
97+
host: "github.com",
7898
skill: "issue-triage",
7999
owner: "monalisa",
80100
repo: "octocat-skills",
@@ -107,6 +127,7 @@ func TestRecordInstall(t *testing.T) {
107127
require.NoError(t, err)
108128
t.Cleanup(unlock)
109129
},
130+
host: "github.com",
110131
skill: "code-review",
111132
owner: "monalisa",
112133
repo: "octocat-skills",
@@ -123,6 +144,7 @@ func TestRecordInstall(t *testing.T) {
123144
require.NoError(t, os.MkdirAll(filepath.Dir(lockPath), 0o755))
124145
require.NoError(t, os.WriteFile(lockPath, []byte("{invalid json"), 0o644))
125146
},
147+
host: "github.com",
126148
skill: "code-review",
127149
owner: "monalisa",
128150
repo: "octocat-skills",
@@ -145,6 +167,7 @@ func TestRecordInstall(t *testing.T) {
145167
data, _ := json.Marshal(file{Version: 999, Skills: map[string]entry{"old-skill": {}}})
146168
require.NoError(t, os.WriteFile(lockPath, data, 0o644))
147169
},
170+
host: "github.com",
148171
skill: "code-review",
149172
owner: "monalisa",
150173
repo: "octocat-skills",
@@ -166,7 +189,7 @@ func TestRecordInstall(t *testing.T) {
166189
tt.setup(t)
167190
}
168191

169-
err := RecordInstall(tt.skill, tt.owner, tt.repo, tt.skillPath, tt.treeSHA, tt.pinnedRef)
192+
err := RecordInstall(tt.host, tt.skill, tt.owner, tt.repo, tt.skillPath, tt.treeSHA, tt.pinnedRef)
170193
if tt.wantErr {
171194
require.Error(t, err)
172195
return
@@ -181,10 +204,10 @@ func TestRecordInstall(t *testing.T) {
181204
t.Run("update preserves InstalledAt and updates treeSHA", func(t *testing.T) {
182205
lockPath := setupTestHome(t)
183206

184-
require.NoError(t, RecordInstall("code-review", "monalisa", "octocat-skills", "skills/code-review/SKILL.md", "old-sha", ""))
207+
require.NoError(t, RecordInstall("github.com", "code-review", "monalisa", "octocat-skills", "skills/code-review/SKILL.md", "old-sha", ""))
185208
firstInstalledAt := readTestLockfile(t, lockPath).Skills["code-review"].InstalledAt
186209

187-
require.NoError(t, RecordInstall("code-review", "monalisa", "octocat-skills", "skills/code-review/SKILL.md", "new-sha", ""))
210+
require.NoError(t, RecordInstall("github.com", "code-review", "monalisa", "octocat-skills", "skills/code-review/SKILL.md", "new-sha", ""))
188211
entry := readTestLockfile(t, lockPath).Skills["code-review"]
189212

190213
assert.Equal(t, "new-sha", entry.SkillFolderHash, "treeSHA should be updated")

internal/skills/source/source.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"strings"
66

7+
ghauth "github.com/cli/go-gh/v2/pkg/auth"
8+
79
"github.com/cli/cli/v2/internal/ghrepo"
810
)
911

@@ -48,16 +50,21 @@ func ParseMetadataRepo(meta map[string]interface{}) (ghrepo.Interface, bool, err
4850
return repo, true, nil
4951
}
5052

51-
// ValidateSupportedHost rejects hosts that are not supported in public preview.
53+
// ValidateSupportedHost rejects hosts that are not supported.
54+
// Supported hosts are github.com and GHEC with data residency (*.ghe.com).
55+
// GitHub Enterprise Server is not currently supported.
5256
func ValidateSupportedHost(host string) error {
5357
host = normalizeHost(host)
5458
if host == "" {
5559
return fmt.Errorf("could not determine repository host")
5660
}
57-
if host != SupportedHost {
58-
return fmt.Errorf("GitHub Skills currently supports only %s as a host; got %s", SupportedHost, host)
61+
if host == SupportedHost || ghauth.IsTenancy(host) {
62+
return nil
63+
}
64+
if ghauth.IsEnterprise(host) {
65+
return fmt.Errorf("GitHub Skills does not currently support GitHub Enterprise Server; got %s", host)
5966
}
60-
return nil
67+
return fmt.Errorf("unsupported host for GitHub Skills: %s", host)
6168
}
6269

6370
func normalizeHost(host string) string {

internal/skills/source/source_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,7 @@ func TestParseMetadataRepo(t *testing.T) {
7272

7373
func TestValidateSupportedHost(t *testing.T) {
7474
require.NoError(t, ValidateSupportedHost("github.com"))
75-
require.ErrorContains(t, ValidateSupportedHost("acme.ghes.com"), "supports only github.com")
75+
require.NoError(t, ValidateSupportedHost("mycompany.ghe.com"), "GHEC data residency tenancy hosts should be accepted")
76+
require.ErrorContains(t, ValidateSupportedHost("acme.ghes.com"), "does not currently support GitHub Enterprise Server")
77+
require.ErrorContains(t, ValidateSupportedHost("github.localhost"), "unsupported host")
7678
}

pkg/cmd/skills/install/install_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1173,7 +1173,7 @@ func TestInstallRun(t *testing.T) {
11731173
SkillName: "git-commit",
11741174
}
11751175
},
1176-
wantErr: "supports only github.com",
1176+
wantErr: "does not currently support GitHub Enterprise Server",
11771177
},
11781178
{
11791179
name: "select all skills in interactive prompt",

pkg/cmd/skills/preview/preview_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ func TestPreviewRun_UnsupportedHost(t *testing.T) {
413413
repo: ghrepo.NewWithHost("github", "awesome-copilot", "acme.ghes.com"),
414414
Telemetry: &telemetry.NoOpService{},
415415
})
416-
require.ErrorContains(t, err, "supports only github.com")
416+
require.ErrorContains(t, err, "does not currently support GitHub Enterprise Server")
417417
}
418418

419419
func TestPreviewRun_Interactive(t *testing.T) {

pkg/cmd/skills/publish/publish.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ func detectGitHubRemote(gitClient *git.Client, dir string) (*gitHubRemote, error
968968
}
969969

970970
// parseGitHubURL extracts owner/repo from a GitHub remote URL.
971-
// Only GitHub.com URLs are recognized.
971+
// Only github.com and GHEC data residency (*.ghe.com) URLs are recognized.
972972
func parseGitHubURL(rawURL string) (ghrepo.Interface, error) {
973973
u, err := git.ParseURL(rawURL)
974974
if err != nil {

pkg/cmd/skills/publish/publish_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func TestPublishRun_UnsupportedHost(t *testing.T) {
171171
HttpClient: func() (*http.Client, error) { return nil, nil },
172172
host: "acme.ghes.com",
173173
})
174-
require.ErrorContains(t, err, "supports only github.com")
174+
require.ErrorContains(t, err, "does not currently support GitHub Enterprise Server")
175175
}
176176

177177
func TestPublishRun(t *testing.T) {

pkg/cmd/skills/search/search_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestSearchRun_UnsupportedHost(t *testing.T) {
3333
HttpClient: func() (*http.Client, error) { return &http.Client{}, nil },
3434
Config: func() (gh.Config, error) { return cfg, nil },
3535
})
36-
require.ErrorContains(t, err, "supports only github.com")
36+
require.ErrorContains(t, err, "does not currently support GitHub Enterprise Server")
3737
}
3838

3939
func TestNewCmdSearch(t *testing.T) {

0 commit comments

Comments
 (0)