Skip to content

Commit

Permalink
Merge pull request #9 from hashtagchris/issue1
Browse files Browse the repository at this point in the history
Create the organization if it doesn't exist
  • Loading branch information
hashtagchris committed Sep 23, 2020
2 parents 9c28fe3 + d39e48f commit 474c0d7
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 11 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ When there are machines which have access to both the public internet and the GH
- `destination-url` _(required)_
The URL of the GHES instance to sync repositories onto.
- `destination-token` _(required)_
A personal access token to authenticate against the GHES instance when uploading repositories.
A personal access token to authenticate against the GHES instance when uploading repositories. See [Destination token scopes](#destination-token-scopes) below.
- `repo-name` _(optional)_
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`
- `repo-name-list` _(optional)_
Expand All @@ -41,9 +41,9 @@ When there are machines which have access to both the public internet and the GH

```
actions-sync sync \
--cache-dir "tmp/cache" \
--cache-dir "/tmp/cache" \
--destination-token "token" \
--destination-url "www.example.com" \
--destination-url "https://www.example.com" \
--repo-name actions/setup-node
```

Expand Down Expand Up @@ -89,17 +89,22 @@ When no machine has access to both the public internet and the GHES instance:
- `destination-url` _(required)_
The URL of the GHES instance to sync repositories onto.
- `destination-token` _(required)_
A personal access token to authenticate against the GHES instance when uploading repositories.
A personal access token to authenticate against the GHES instance when uploading repositories. See [Destination token scopes](#destination-token-scopes) below.
- `repo-name`, `repo-name-list` or `repo-name-list-file` _(optional)_
Limit push to specific repositories in the cache directory.

**Example Usage:**

```
bin/actions-sync push \
--cache-dir "/tmp/cache" \
--destination-token "token" \
--destination-url "http://www.example.com"
--destination-url "https://www.example.com"
```

## Destination token scopes

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.

## Contributing

Expand Down
21 changes: 21 additions & 0 deletions script/test-build
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,27 @@ function test_push() {
assert_dest_sha "org/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo1 passed in repo flag"
assert_dest_sha "org/repo2" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "org/repo2 not updated despite cache"

# Push to pre-existing org
setup_cache "org-already-exists/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
setup_dest "org-already-exists/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

push "pushing to existing org"
assert_dest_sha "org-already-exists/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/new-repo"

# Push to pre-existing repo
setup_cache "org-already-exists/repo-already-exists:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
setup_dest "org-already-exists/repo-already-exists:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

push "pushing to existing repo"
assert_dest_sha "org-already-exists/repo-already-exists" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/repo-already-exists"

# Push to repo in user's account
setup_cache "monalisa/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
setup_dest "monalisa/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

push "pushing to authenticated user's account"
assert_dest_sha "monalisa/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating monalisa/new-repo"

echo "all push tests passed successfully"
}

Expand Down
59 changes: 53 additions & 6 deletions src/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path"
"strings"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
Expand Down Expand Up @@ -114,31 +115,77 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC
return nil
}

func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName, orgName string) (*github.Repository, error) {
func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName, ownerName string) (*github.Repository, error) {
repo := &github.Repository{
Name: github.String(repoName),
HasIssues: github.Bool(false),
HasWiki: github.Bool(false),
HasPages: github.Bool(false),
HasProjects: github.Bool(false),
}
ghRepo, resp, err := client.Repositories.Create(ctx, orgName, repo)
if resp != nil && resp.StatusCode == 422 {
ghRepo, _, err = client.Repositories.Get(ctx, orgName, repoName)

currentUser, _, err := client.Users.Get(ctx, "")
if err != nil {
return nil, errors.Wrap(err, "error retrieving authenticated user")
}
if currentUser == nil || currentUser.Login == nil {
return nil, errors.New("error retrieving authenticated user's login name")
}

// check if the owner refers to the authenticated user or an organization.
var createRepoOrgName string
if strings.EqualFold(*currentUser.Login, ownerName) {
// we'll create the repo under the authenticated user's account.
createRepoOrgName = ""
} else {
// ensure the org exists.
createRepoOrgName = ownerName
_, err := getOrCreateGitHubOrg(ctx, client, ownerName, *currentUser.Login)
if err != nil {
return nil, err
}
}

ghRepo, resp, err := client.Repositories.Create(ctx, createRepoOrgName, repo)
if err == nil {
fmt.Printf("Created repo `%s/%s`\n", ownerName, repoName)
} else if resp != nil && resp.StatusCode == 422 {
ghRepo, _, err = client.Repositories.Get(ctx, ownerName, repoName)
}
if err != nil {
return nil, errors.Wrap(err, "error creating repository")
return nil, errors.Wrapf(err, "error creating repository %s/%s", ownerName, repoName)
}
if ghRepo == nil {
return nil, errors.New("error repository is nil")
}
return ghRepo, nil
}

func getOrCreateGitHubOrg(ctx context.Context, client *github.Client, orgName, admin string) (*github.Organization, error) {
org := &github.Organization{Login: &orgName}

var getErr error
ghOrg, _, createErr := client.Admin.CreateOrg(ctx, org, admin)
if createErr == nil {
fmt.Printf("Created organization `%s` (admin: %s)\n", orgName, admin)
} else {
// Regardless of why create failed, see if we can retrieve the org
ghOrg, _, getErr = client.Organizations.Get(ctx, orgName)
}
if createErr != nil && getErr != nil {
return nil, errors.Wrapf(createErr, "error creating organization %s", orgName)
}
if ghOrg == nil {
return nil, errors.New("error organization is nil")
}

return ghOrg, nil
}

func syncWithCachedRepository(ctx context.Context, flags *PushFlags, ghRepo *github.Repository, repoDir string, gitimpl GitImplementation) error {
gitRepo, err := gitimpl.NewGitRepository(repoDir)
if err != nil {
return errors.Wrapf(err, "error opening git repository %s", flags.CacheDir)
return errors.Wrapf(err, "error opening git repository %s", repoDir)
}
_ = gitRepo.DeleteRemote("ghes")
remote, err := gitRepo.CreateRemote(&config.RemoteConfig{
Expand Down
131 changes: 131 additions & 0 deletions test/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"path"
Expand All @@ -11,6 +12,10 @@ import (
"github.com/gorilla/mux"
)

var authenticatedLogin string = "monalisa"
var existingOrg string = "org-already-exists"
var existingRepo string = "repo-already-exists"

func main() {
var port, gitDaemonURL string
flag.StringVar(&port, "p", "", "")
Expand All @@ -19,6 +24,73 @@ func main() {

r := mux.NewRouter()
r.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {})

r.HandleFunc("/api/v3/user", func(w http.ResponseWriter, r *http.Request) {
currentUser := github.User{Login: &authenticatedLogin}
b, _ := json.Marshal(currentUser)
_, err := w.Write(b)
if err != nil {
panic(err)
}
})

r.HandleFunc("/api/v3/admin/organizations", func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var orgReq struct {
Login string `json:"login,omitempty"`
Admin string `json:"admin,omitempty"`
}
err = json.Unmarshal(b, &orgReq)
if err != nil {
panic(err)
}

if orgReq.Login == authenticatedLogin {
w.WriteHeader(http.StatusUnprocessableEntity)
_, err := w.Write([]byte(fmt.Sprintf("%s is a user, not an organization", orgReq.Login)))
if err != nil {
panic(err)
}
}

if orgReq.Login == existingOrg {
w.WriteHeader(http.StatusUnprocessableEntity)
_, err := w.Write([]byte(fmt.Sprintf("Organization %s already exists", orgReq.Login)))
if err != nil {
panic(err)
}
}

org := github.Organization{Login: &orgReq.Login}
b, _ = json.Marshal(org)
_, err = w.Write(b)
if err != nil {
panic(err)
}
}).Methods("POST")

r.HandleFunc("/api/v3/orgs/{org}", func(w http.ResponseWriter, r *http.Request) {
orgName := mux.Vars(r)["org"]

if orgName != existingOrg {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte(fmt.Sprintf("Organization %s not found", orgName)))
if err != nil {
panic(err)
}
}

org := github.Organization{Login: &orgName}
b, _ := json.Marshal(org)
_, err := w.Write(b)
if err != nil {
panic(err)
}
})

r.HandleFunc("/api/v3/orgs/{org}/repos", func(w http.ResponseWriter, r *http.Request) {
orgName := mux.Vars(r)["org"]
b, err := ioutil.ReadAll(r.Body)
Expand All @@ -33,6 +105,14 @@ func main() {
panic(err)
}

if repoReq.Name == "repo-already-exists" {
w.WriteHeader(http.StatusUnprocessableEntity)
_, err := w.Write([]byte(fmt.Sprintf("Repo %s already exists", repoReq.Name)))
if err != nil {
panic(err)
}
}

cloneURL := gitDaemonURL + path.Join(orgName, repoReq.Name, ".git")
repo := github.Repository{Name: &repoReq.Name, CloneURL: &cloneURL}
b, _ = json.Marshal(repo)
Expand All @@ -42,6 +122,57 @@ func main() {
}
}).Methods("POST")

r.HandleFunc("/api/v3/user/repos", func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var repoReq struct {
Name string `json:"name,omitempty"`
}
err = json.Unmarshal(b, &repoReq)
if err != nil {
panic(err)
}

if repoReq.Name == existingRepo {
w.WriteHeader(http.StatusUnprocessableEntity)
_, err := w.Write([]byte(fmt.Sprintf("Repo %s already exists", repoReq.Name)))
if err != nil {
panic(err)
}
}

cloneURL := gitDaemonURL + path.Join(authenticatedLogin, repoReq.Name, ".git")
repo := github.Repository{Name: &repoReq.Name, CloneURL: &cloneURL}
b, _ = json.Marshal(repo)
_, err = w.Write(b)
if err != nil {
panic(err)
}
}).Methods("POST")

r.HandleFunc("/api/v3/repos/{owner}/{repo}", func(w http.ResponseWriter, r *http.Request) {
ownerName := mux.Vars(r)["owner"]
repoName := mux.Vars(r)["repo"]

if repoName != existingRepo {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte(fmt.Sprintf("Repo %s not found", repoName)))
if err != nil {
panic(err)
}
}

cloneURL := gitDaemonURL + path.Join(ownerName, repoName, ".git")
org := github.Repository{Name: &repoName, CloneURL: &cloneURL}
b, _ := json.Marshal(org)
_, err := w.Write(b)
if err != nil {
panic(err)
}
})

err := http.ListenAndServe(":"+port, r)
if err != nil {
panic(err)
Expand Down

0 comments on commit 474c0d7

Please sign in to comment.