Merge pull request #9 from hashtagchris/issue1
Create the organization if it doesn't exist
This commit is contained in:
@@ -29,7 +29,7 @@ When there are machines which have access to both the public internet and the GH
|
|||||||
- `destination-url` _(required)_
|
- `destination-url` _(required)_
|
||||||
The URL of the GHES instance to sync repositories onto.
|
The URL of the GHES instance to sync repositories onto.
|
||||||
- `destination-token` _(required)_
|
- `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)_
|
- `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`
|
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)_
|
- `repo-name-list` _(optional)_
|
||||||
@@ -41,9 +41,9 @@ When there are machines which have access to both the public internet and the GH
|
|||||||
|
|
||||||
```
|
```
|
||||||
actions-sync sync \
|
actions-sync sync \
|
||||||
--cache-dir "tmp/cache" \
|
--cache-dir "/tmp/cache" \
|
||||||
--destination-token "token" \
|
--destination-token "token" \
|
||||||
--destination-url "www.example.com" \
|
--destination-url "https://www.example.com" \
|
||||||
--repo-name actions/setup-node
|
--repo-name actions/setup-node
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,7 +89,9 @@ When no machine has access to both the public internet and the GHES instance:
|
|||||||
- `destination-url` _(required)_
|
- `destination-url` _(required)_
|
||||||
The URL of the GHES instance to sync repositories onto.
|
The URL of the GHES instance to sync repositories onto.
|
||||||
- `destination-token` _(required)_
|
- `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:**
|
**Example Usage:**
|
||||||
|
|
||||||
@@ -97,9 +99,12 @@ When no machine has access to both the public internet and the GHES instance:
|
|||||||
bin/actions-sync push \
|
bin/actions-sync push \
|
||||||
--cache-dir "/tmp/cache" \
|
--cache-dir "/tmp/cache" \
|
||||||
--destination-token "token" \
|
--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
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -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/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"
|
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"
|
echo "all push tests passed successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+53
-6
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/config"
|
"github.com/go-git/go-git/v5/config"
|
||||||
@@ -114,7 +115,7 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC
|
|||||||
return nil
|
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{
|
repo := &github.Repository{
|
||||||
Name: github.String(repoName),
|
Name: github.String(repoName),
|
||||||
HasIssues: github.Bool(false),
|
HasIssues: github.Bool(false),
|
||||||
@@ -122,12 +123,37 @@ func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName,
|
|||||||
HasPages: github.Bool(false),
|
HasPages: github.Bool(false),
|
||||||
HasProjects: github.Bool(false),
|
HasProjects: github.Bool(false),
|
||||||
}
|
}
|
||||||
ghRepo, resp, err := client.Repositories.Create(ctx, orgName, repo)
|
|
||||||
if resp != nil && resp.StatusCode == 422 {
|
currentUser, _, err := client.Users.Get(ctx, "")
|
||||||
ghRepo, _, err = client.Repositories.Get(ctx, orgName, repoName)
|
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 {
|
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 {
|
if ghRepo == nil {
|
||||||
return nil, errors.New("error repository is nil")
|
return nil, errors.New("error repository is nil")
|
||||||
@@ -135,10 +161,31 @@ func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName,
|
|||||||
return ghRepo, 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 {
|
func syncWithCachedRepository(ctx context.Context, flags *PushFlags, ghRepo *github.Repository, repoDir string, gitimpl GitImplementation) error {
|
||||||
gitRepo, err := gitimpl.NewGitRepository(repoDir)
|
gitRepo, err := gitimpl.NewGitRepository(repoDir)
|
||||||
if err != nil {
|
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")
|
_ = gitRepo.DeleteRemote("ghes")
|
||||||
remote, err := gitRepo.CreateRemote(&config.RemoteConfig{
|
remote, err := gitRepo.CreateRemote(&config.RemoteConfig{
|
||||||
|
|||||||
+131
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
@@ -11,6 +12,10 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var authenticatedLogin string = "monalisa"
|
||||||
|
var existingOrg string = "org-already-exists"
|
||||||
|
var existingRepo string = "repo-already-exists"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var port, gitDaemonURL string
|
var port, gitDaemonURL string
|
||||||
flag.StringVar(&port, "p", "", "")
|
flag.StringVar(&port, "p", "", "")
|
||||||
@@ -19,6 +24,73 @@ func main() {
|
|||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {})
|
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) {
|
r.HandleFunc("/api/v3/orgs/{org}/repos", func(w http.ResponseWriter, r *http.Request) {
|
||||||
orgName := mux.Vars(r)["org"]
|
orgName := mux.Vars(r)["org"]
|
||||||
b, err := ioutil.ReadAll(r.Body)
|
b, err := ioutil.ReadAll(r.Body)
|
||||||
@@ -33,6 +105,14 @@ func main() {
|
|||||||
panic(err)
|
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")
|
cloneURL := gitDaemonURL + path.Join(orgName, repoReq.Name, ".git")
|
||||||
repo := github.Repository{Name: &repoReq.Name, CloneURL: &cloneURL}
|
repo := github.Repository{Name: &repoReq.Name, CloneURL: &cloneURL}
|
||||||
b, _ = json.Marshal(repo)
|
b, _ = json.Marshal(repo)
|
||||||
@@ -42,6 +122,57 @@ func main() {
|
|||||||
}
|
}
|
||||||
}).Methods("POST")
|
}).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)
|
err := http.ListenAndServe(":"+port, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
Reference in New Issue
Block a user