Skip to content

Commit d39e48f

Browse files
committed
Create org if necessary
1 parent 4e5bb47 commit d39e48f

File tree

4 files changed

+193
-23
lines changed

4 files changed

+193
-23
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ When there are machines which have access to both the public internet and the GH
2929
- `destination-url` _(required)_
3030
The URL of the GHES instance to sync repositories onto.
3131
- `destination-token` _(required)_
32-
A personal access token to authenticate against the GHES instance when uploading repositories.
32+
A personal access token to authenticate against the GHES instance when uploading repositories. See [Destination token scopes](#destination-token-scopes) below.
3333
- `repo-name` _(optional)_
3434
A single repository to be synced. In the format of `owner/repo`. Optionally if you wish the repository to be named different on your GHES instance you can provide an alias in the format: `upstream_owner/upstream_repo:destination_owner/destination_repo`
3535
- `repo-name-list` _(optional)_
@@ -89,7 +89,9 @@ When no machine has access to both the public internet and the GHES instance:
8989
- `destination-url` _(required)_
9090
The URL of the GHES instance to sync repositories onto.
9191
- `destination-token` _(required)_
92-
A personal access token to authenticate against the GHES instance when uploading repositories.
92+
A personal access token to authenticate against the GHES instance when uploading repositories. See [Destination token scopes](#destination-token-scopes) below.
93+
- `repo-name`, `repo-name-list` or `repo-name-list-file` _(optional)_
94+
Limit push to specific repositories in the cache directory.
9395

9496
**Example Usage:**
9597

@@ -100,6 +102,9 @@ When no machine has access to both the public internet and the GHES instance:
100102
--destination-url "https://www.example.com"
101103
```
102104

105+
## Destination token scopes
106+
107+
When creating a personal access token include the `repo` scope. Include the `site_admin` scope (optional) if you want organizations to be created as necessary.
103108

104109
## Contributing
105110

script/test-build

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,27 @@ function test_push() {
102102
assert_dest_sha "org/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo1 passed in repo flag"
103103
assert_dest_sha "org/repo2" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "org/repo2 not updated despite cache"
104104

105+
# Push to pre-existing org
106+
setup_cache "org-already-exists/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
107+
setup_dest "org-already-exists/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
108+
109+
push "pushing to existing org"
110+
assert_dest_sha "org-already-exists/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/new-repo"
111+
112+
# Push to pre-existing repo
113+
setup_cache "org-already-exists/repo-already-exists:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
114+
setup_dest "org-already-exists/repo-already-exists:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
115+
116+
push "pushing to existing repo"
117+
assert_dest_sha "org-already-exists/repo-already-exists" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/repo-already-exists"
118+
119+
# Push to repo in user's account
120+
setup_cache "monalisa/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
121+
setup_dest "monalisa/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
122+
123+
push "pushing to authenticated user's account"
124+
assert_dest_sha "monalisa/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating monalisa/new-repo"
125+
105126
echo "all push tests passed successfully"
106127
}
107128

src/push.go

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC
103103
}
104104

105105
fmt.Printf("syncing `%s`\n", nwo)
106-
ghRepo, err := getOrCreateGitHubRepo(ctx, flags, ghClient, bareRepoName, ownerName)
106+
ghRepo, err := getOrCreateGitHubRepo(ctx, ghClient, bareRepoName, ownerName)
107107
if err != nil {
108108
return errors.Wrapf(err, "error creating github repository `%s`", nwo)
109109
}
@@ -115,7 +115,7 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC
115115
return nil
116116
}
117117

118-
func getOrCreateGitHubRepo(ctx context.Context, flags *PushFlags, client *github.Client, repoName, ownerName string) (*github.Repository, error) {
118+
func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName, ownerName string) (*github.Repository, error) {
119119
repo := &github.Repository{
120120
Name: github.String(repoName),
121121
HasIssues: github.Bool(false),
@@ -124,39 +124,64 @@ func getOrCreateGitHubRepo(ctx context.Context, flags *PushFlags, client *github
124124
HasProjects: github.Bool(false),
125125
}
126126

127-
orgName := ownerName
127+
currentUser, _, err := client.Users.Get(ctx, "")
128+
if err != nil {
129+
return nil, errors.Wrap(err, "error retrieving authenticated user")
130+
}
131+
if currentUser == nil || currentUser.Login == nil {
132+
return nil, errors.New("error retrieving authenticated user's login name")
133+
}
128134

129-
// Confirm the org exists
130-
_, resp, err := client.Organizations.Get(ctx, orgName)
131-
if resp != nil && resp.StatusCode == 404 {
132-
// Check if the destination owner matches the authenticated user. (best effort)
133-
currentUser, _, _ := client.Users.Get(ctx, "")
134-
if currentUser != nil && strings.EqualFold(*currentUser.Login, ownerName) {
135-
// create the new repo under the authenticated user's account.
136-
orgName = ""
137-
err = nil
138-
} else {
139-
return nil, errors.Errorf("Organization `%s` doesn't exist at %s. You must create it first.", ownerName, flags.BaseURL)
135+
// check if the owner refers to the authenticated user or an organization.
136+
var createRepoOrgName string
137+
if strings.EqualFold(*currentUser.Login, ownerName) {
138+
// we'll create the repo under the authenticated user's account.
139+
createRepoOrgName = ""
140+
} else {
141+
// ensure the org exists.
142+
createRepoOrgName = ownerName
143+
_, err := getOrCreateGitHubOrg(ctx, client, ownerName, *currentUser.Login)
144+
if err != nil {
145+
return nil, err
140146
}
141147
}
142-
if err != nil {
143-
return nil, errors.Wrapf(err, "error retrieving organization %s", ownerName)
144-
}
145148

146-
// Create the repo if necessary
147-
ghRepo, resp, err := client.Repositories.Create(ctx, orgName, repo)
148-
if resp != nil && resp.StatusCode == 422 {
149+
ghRepo, resp, err := client.Repositories.Create(ctx, createRepoOrgName, repo)
150+
if err == nil {
151+
fmt.Printf("Created repo `%s/%s`\n", ownerName, repoName)
152+
} else if resp != nil && resp.StatusCode == 422 {
149153
ghRepo, _, err = client.Repositories.Get(ctx, ownerName, repoName)
150154
}
151155
if err != nil {
152-
return nil, errors.Wrap(err, "error creating repository")
156+
return nil, errors.Wrapf(err, "error creating repository %s/%s", ownerName, repoName)
153157
}
154158
if ghRepo == nil {
155159
return nil, errors.New("error repository is nil")
156160
}
157161
return ghRepo, nil
158162
}
159163

164+
func getOrCreateGitHubOrg(ctx context.Context, client *github.Client, orgName, admin string) (*github.Organization, error) {
165+
org := &github.Organization{Login: &orgName}
166+
167+
var getErr error
168+
ghOrg, _, createErr := client.Admin.CreateOrg(ctx, org, admin)
169+
if createErr == nil {
170+
fmt.Printf("Created organization `%s` (admin: %s)\n", orgName, admin)
171+
} else {
172+
// Regardless of why create failed, see if we can retrieve the org
173+
ghOrg, _, getErr = client.Organizations.Get(ctx, orgName)
174+
}
175+
if createErr != nil && getErr != nil {
176+
return nil, errors.Wrapf(createErr, "error creating organization %s", orgName)
177+
}
178+
if ghOrg == nil {
179+
return nil, errors.New("error organization is nil")
180+
}
181+
182+
return ghOrg, nil
183+
}
184+
160185
func syncWithCachedRepository(ctx context.Context, flags *PushFlags, ghRepo *github.Repository, repoDir string, gitimpl GitImplementation) error {
161186
gitRepo, err := gitimpl.NewGitRepository(repoDir)
162187
if err != nil {

test/github.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"encoding/json"
55
"flag"
6+
"fmt"
67
"io/ioutil"
78
"net/http"
89
"path"
@@ -11,6 +12,10 @@ import (
1112
"github.com/gorilla/mux"
1213
)
1314

15+
var authenticatedLogin string = "monalisa"
16+
var existingOrg string = "org-already-exists"
17+
var existingRepo string = "repo-already-exists"
18+
1419
func main() {
1520
var port, gitDaemonURL string
1621
flag.StringVar(&port, "p", "", "")
@@ -20,9 +25,64 @@ func main() {
2025
r := mux.NewRouter()
2126
r.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {})
2227

28+
r.HandleFunc("/api/v3/user", func(w http.ResponseWriter, r *http.Request) {
29+
currentUser := github.User{Login: &authenticatedLogin}
30+
b, _ := json.Marshal(currentUser)
31+
_, err := w.Write(b)
32+
if err != nil {
33+
panic(err)
34+
}
35+
})
36+
37+
r.HandleFunc("/api/v3/admin/organizations", func(w http.ResponseWriter, r *http.Request) {
38+
b, err := ioutil.ReadAll(r.Body)
39+
if err != nil {
40+
panic(err)
41+
}
42+
var orgReq struct {
43+
Login string `json:"login,omitempty"`
44+
Admin string `json:"admin,omitempty"`
45+
}
46+
err = json.Unmarshal(b, &orgReq)
47+
if err != nil {
48+
panic(err)
49+
}
50+
51+
if orgReq.Login == authenticatedLogin {
52+
w.WriteHeader(http.StatusUnprocessableEntity)
53+
_, err := w.Write([]byte(fmt.Sprintf("%s is a user, not an organization", orgReq.Login)))
54+
if err != nil {
55+
panic(err)
56+
}
57+
}
58+
59+
if orgReq.Login == existingOrg {
60+
w.WriteHeader(http.StatusUnprocessableEntity)
61+
_, err := w.Write([]byte(fmt.Sprintf("Organization %s already exists", orgReq.Login)))
62+
if err != nil {
63+
panic(err)
64+
}
65+
}
66+
67+
org := github.Organization{Login: &orgReq.Login}
68+
b, _ = json.Marshal(org)
69+
_, err = w.Write(b)
70+
if err != nil {
71+
panic(err)
72+
}
73+
}).Methods("POST")
74+
2375
r.HandleFunc("/api/v3/orgs/{org}", func(w http.ResponseWriter, r *http.Request) {
2476
orgName := mux.Vars(r)["org"]
2577

78+
if orgName != existingOrg {
79+
w.WriteHeader(http.StatusNotFound)
80+
_, err := w.Write([]byte(fmt.Sprintf("Organization %s not found", orgName)))
81+
if err != nil {
82+
panic(err)
83+
}
84+
}
85+
2686
org := github.Organization{Login: &orgName}
2787
b, _ := json.Marshal(org)
2888
_, err := w.Write(b)
@@ -45,6 +105,14 @@ func main() {
45105
panic(err)
46106
}
47107

108+
if repoReq.Name == "repo-already-exists" {
109+
w.WriteHeader(http.StatusUnprocessableEntity)
110+
_, err := w.Write([]byte(fmt.Sprintf("Repo %s already exists", repoReq.Name)))
111+
if err != nil {
112+
panic(err)
113+
}
114+
}
115+
48116
cloneURL := gitDaemonURL + path.Join(orgName, repoReq.Name, ".git")
49117
repo := github.Repository{Name: &repoReq.Name, CloneURL: &cloneURL}
50118
b, _ = json.Marshal(repo)
@@ -54,6 +122,57 @@ func main() {
54122
}
55123
}).Methods("POST")
56124

125+
r.HandleFunc("/api/v3/user/repos", func(w http.ResponseWriter, r *http.Request) {
126+
b, err := ioutil.ReadAll(r.Body)
127+
if err != nil {
128+
panic(err)
129+
}
130+
var repoReq struct {
131+
Name string `json:"name,omitempty"`
132+
}
133+
err = json.Unmarshal(b, &repoReq)
134+
if err != nil {
135+
panic(err)
136+
}
137+
138+
if repoReq.Name == existingRepo {
139+
w.WriteHeader(http.StatusUnprocessableEntity)
140+
_, err := w.Write([]byte(fmt.Sprintf("Repo %s already exists", repoReq.Name)))
141+
if err != nil {
142+
panic(err)
143+
}
144+
}
145+
146+
cloneURL := gitDaemonURL + path.Join(authenticatedLogin, repoReq.Name, ".git")
147+
repo := github.Repository{Name: &repoReq.Name, CloneURL: &cloneURL}
148+
b, _ = json.Marshal(repo)
149+
_, err = w.Write(b)
150+
if err != nil {
151+
panic(err)
152+
}
153+
}).Methods("POST")
154+
155+
r.HandleFunc("/api/v3/repos/{owner}/{repo}", func(w http.ResponseWriter, r *http.Request) {
156+
ownerName := mux.Vars(r)["owner"]
157+
repoName := mux.Vars(r)["repo"]
158+
159+
if repoName != existingRepo {
160+
w.WriteHeader(http.StatusNotFound)
161+
_, err := w.Write([]byte(fmt.Sprintf("Repo %s not found", repoName)))
162+
if err != nil {
163+
panic(err)
164+
}
165+
}
166+
167+
cloneURL := gitDaemonURL + path.Join(ownerName, repoName, ".git")
168+
org := github.Repository{Name: &repoName, CloneURL: &cloneURL}
169+
b, _ := json.Marshal(org)
170+
_, err := w.Write(b)
171+
if err != nil {
172+
panic(err)
173+
}
174+
})
175+
57176
err := http.ListenAndServe(":"+port, r)
58177
if err != nil {
59178
panic(err)

0 commit comments

Comments
 (0)