Skip to content

Commit

Permalink
Merge pull request #48 from github/update-bundled-action
Browse files Browse the repository at this point in the history
Make various improvements to the sync tool to handle updating a bundled CodeQL Action.
  • Loading branch information
chrisgavin authored Nov 14, 2020
2 parents 0375789 + ef02b83 commit 9436204
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 23 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ From a machine with access to both GitHub.com and GitHub Enterprise Server use t

**Required Arguments:**
* `--destination-url` - The URL of the GitHub Enterprise Server instance to push the Action to.
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist or that you are not an owner of, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.

**Optional Arguments:**
* `--cache-dir` - A temporary directory in which to store data downloaded from GitHub.com before it is uploaded to GitHub Enterprise Server. If not specified a directory next to the sync tool will be used.
* `--source-token` - A token to access the API of GitHub.com. This is normally not required, but can be provided if you have issues with API rate limiting. The token does not need to have any scopes.
* `--destination-repository` - The name of the repository in which to create or update the CodeQL Action. If not specified `github/codeql-action` will be used.
* `--actions-admin-user` - The name of the Actions admin user, which will be used if you are updating the bundled CodeQL Action. If not specified `actions-admin` will be used.
* `--force` - By default the tool will not overwrite existing repositories. Providing this flag will allow it to.
* `--push-ssh` - Push Git contents over SSH rather than HTTPS. To use this option you must have SSH access to your GitHub Enterprise instance configured.

Expand All @@ -42,11 +43,12 @@ Now use the `./codeql-action-sync push` command to upload the CodeQL Action and

**Required Arguments:**
* `--destination-url` - The URL of the GitHub Enterprise Server instance to push the Action to.
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist or that you are not an owner of, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.

**Optional Arguments:**
* `--cache-dir` - The directory to which the Action was previously downloaded.
* `--destination-repository` - The name of the repository in which to create or update the CodeQL Action. If not specified `github/codeql-action` will be used.
* `--actions-admin-user` - The name of the Actions admin user, which will be used if you are updating the bundled CodeQL Action. If not specified `actions-admin` will be used.
* `--force` - By default the tool will not overwrite existing repositories. Providing this flag will allow it to.
* `--push-ssh` - Push Git contents over SSH rather than HTTPS. To use this option you must have SSH access to your GitHub Enterprise instance configured.

Expand Down
4 changes: 3 additions & 1 deletion cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ var pushCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
version.LogVersion()
cacheDirectory := cachedirectory.NewCacheDirectory(rootFlags.cacheDir)
return push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.force, pushFlags.pushSSH)
return push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.actionsAdminUser, pushFlags.force, pushFlags.pushSSH)
},
}

type pushFlagFields struct {
destinationURL string
destinationToken string
destinationRepository string
actionsAdminUser string
force bool
pushSSH bool
}
Expand All @@ -33,6 +34,7 @@ func (f *pushFlagFields) Init(cmd *cobra.Command) {
cmd.Flags().StringVar(&f.destinationToken, "destination-token", "", "A token to access the API on the GitHub Enterprise instance.")
cmd.MarkFlagRequired("destination-token")
cmd.Flags().StringVar(&f.destinationRepository, "destination-repository", "github/codeql-action", "The name of the repository to create on GitHub Enterprise.")
cmd.Flags().StringVar(&f.actionsAdminUser, "actions-admin-user", "actions-admin", "The name of the Actions admin user.")
cmd.Flags().BoolVar(&f.force, "force", false, "Replace the existing repository even if it was not created by the sync tool.")
cmd.Flags().BoolVar(&f.pushSSH, "push-ssh", false, "Push Git contents over SSH rather than HTTPS. To use this option you must have SSH access to your GitHub Enterprise instance configured.")
}
2 changes: 1 addition & 1 deletion cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var syncCmd = &cobra.Command{
if err != nil {
return err
}
err = push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.force, pushFlags.pushSSH)
err = push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.actionsAdminUser, pushFlags.force, pushFlags.pushSSH)
if err != nil {
return err
}
Expand Down
10 changes: 5 additions & 5 deletions internal/githubapiutil/githubapiutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

const xOAuthScopesHeader = "X-OAuth-Scopes"

func MissingAllScopes(response *github.Response, requiredAnyScopes ...string) bool {
func HasAnyScope(response *github.Response, scopes ...string) bool {
if response == nil {
return false
}
Expand All @@ -18,11 +18,11 @@ func MissingAllScopes(response *github.Response, requiredAnyScopes ...string) bo
actualScopes := strings.Split(response.Header.Get(xOAuthScopesHeader), ",")
for _, actualScope := range actualScopes {
actualScope = strings.Trim(actualScope, " ")
for _, requiredAnyScope := range requiredAnyScopes {
if actualScope == requiredAnyScope {
return false
for _, requiredScope := range scopes {
if actualScope == requiredScope {
return true
}
}
}
return true
return false
}
8 changes: 4 additions & 4 deletions internal/githubapiutil/githubapiutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import (
"github.com/google/go-github/v32/github"
)

func TestHasAnyScopes(t *testing.T) {
func TestHasAnyScope(t *testing.T) {
response := github.Response{
Response: &http.Response{Header: http.Header{}},
}

response.Header.Set(xOAuthScopesHeader, "gist, notifications, admin:org, repo")
require.False(t, MissingAllScopes(&response, "public_repo", "repo"))
require.True(t, HasAnyScope(&response, "public_repo", "repo"))

response.Header.Set(xOAuthScopesHeader, "gist, notifications, public_repo, admin:org")
require.False(t, MissingAllScopes(&response, "public_repo", "repo"))
require.True(t, HasAnyScope(&response, "public_repo", "repo"))

response.Header.Set(xOAuthScopesHeader, "gist, notifications, admin:org")
require.True(t, MissingAllScopes(&response, "public_repo", "repo"))
require.False(t, HasAnyScope(&response, "public_repo", "repo"))
}
38 changes: 29 additions & 9 deletions internal/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ type pushService struct {
githubEnterpriseClient *github.Client
destinationRepositoryName string
destinationRepositoryOwner string
destinationToken string
destinationToken *oauth2.Token
actionsAdminUser string
force bool
pushSSH bool
}
Expand Down Expand Up @@ -76,12 +77,25 @@ func (pushService *pushService) createRepository() (*github.Repository, error) {
Name: github.String(pushService.destinationRepositoryOwner),
}, user.GetLogin())
if err != nil {
if response != nil && response.StatusCode == http.StatusNotFound && githubapiutil.MissingAllScopes(response, "site_admin") {
if response != nil && response.StatusCode == http.StatusNotFound && !githubapiutil.HasAnyScope(response, "site_admin") {
return nil, usererrors.New("The destination token you have provided does not have the `site_admin` scope, so the destination organization cannot be created.")
}
return nil, errors.Wrap(err, "Error creating organization.")
}
}

_, response, err = pushService.githubEnterpriseClient.Organizations.GetOrgMembership(pushService.ctx, user.GetLogin(), pushService.destinationRepositoryOwner)
if err != nil && (response == nil || response.StatusCode != http.StatusNotFound) {
return nil, errors.Wrap(err, "Failed to check membership of destination organization.")
}
if err != nil && githubapiutil.HasAnyScope(response, "site_admin") {
log.Debugf("No access to destination organization. Switching to impersonation token for %s...", pushService.actionsAdminUser)
impersonationToken, _, err := pushService.githubEnterpriseClient.Admin.CreateUserImpersonation(pushService.ctx, pushService.actionsAdminUser, &github.ImpersonateUserOptions{Scopes: []string{"public_repo", "workflow"}})
if err != nil {
return nil, errors.Wrap(err, "Failed to impersonate Actions admin user.")
}
pushService.destinationToken.AccessToken = impersonationToken.GetToken()
}
}

repository, response, err := pushService.githubEnterpriseClient.Repositories.Get(pushService.ctx, pushService.destinationRepositoryOwner, pushService.destinationRepositoryName)
Expand All @@ -105,16 +119,20 @@ func (pushService *pushService) createRepository() (*github.Repository, error) {
if response.StatusCode == http.StatusNotFound {
repository, response, err = pushService.githubEnterpriseClient.Repositories.Create(pushService.ctx, destinationOrganization, &desiredRepositoryProperties)
if err != nil {
if response.StatusCode == http.StatusNotFound && githubapiutil.MissingAllScopes(response, "public_repo", "repo") {
if response.StatusCode == http.StatusNotFound && !githubapiutil.HasAnyScope(response, "public_repo", "repo") {
return nil, usererrors.New("The destination token you have provided does not have the `public_repo` scope.")
}
return nil, errors.Wrap(err, "Error creating destination repository.")
}
} else {
repository, response, err = pushService.githubEnterpriseClient.Repositories.Edit(pushService.ctx, pushService.destinationRepositoryOwner, pushService.destinationRepositoryName, &desiredRepositoryProperties)
if err != nil {
if response.StatusCode == http.StatusNotFound && githubapiutil.MissingAllScopes(response, "public_repo", "repo") {
return nil, usererrors.New("The destination token you have provided does not have the `public_repo` scope.")
if response.StatusCode == http.StatusNotFound {
if !githubapiutil.HasAnyScope(response, "public_repo", "repo") {
return nil, usererrors.New("The destination token you have provided does not have the `public_repo` scope.")
} else {
return nil, fmt.Errorf("You don't have permission to update the repository at %s/%s. If you wish to update the bundled CodeQL Action please provide a token with the `site_admin` scope.", pushService.destinationRepositoryOwner, pushService.destinationRepositoryName)
}
}
return nil, errors.Wrap(err, "Error updating destination repository.")
}
Expand Down Expand Up @@ -145,7 +163,7 @@ func (pushService *pushService) pushGit(repository *github.Repository, initialPu

credentials := &githttp.BasicAuth{
Username: "x-access-token",
Password: pushService.destinationToken,
Password: pushService.destinationToken.AccessToken,
}
if pushService.pushSSH {
// Use the SSH key from the environment.
Expand Down Expand Up @@ -327,7 +345,7 @@ func (pushService *pushService) pushReleases() error {
return nil
}

func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, destinationURL string, destinationToken string, destinationRepository string, force bool, pushSSH bool) error {
func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, destinationURL string, destinationToken string, destinationRepository string, actionsAdminUser string, force bool, pushSSH bool) error {
err := cacheDirectory.CheckOrCreateVersionFile(false, version.Version())
if err != nil {
return err
Expand All @@ -338,8 +356,9 @@ func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, des
}

destinationURL = strings.TrimRight(destinationURL, "/")
token := oauth2.Token{AccessToken: destinationToken}
tokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: destinationToken},
&token,
)
tokenClient := oauth2.NewClient(ctx, tokenSource)
client, err := github.NewEnterpriseClient(destinationURL+"/api/v3", destinationURL+"/api/uploads", tokenClient)
Expand All @@ -357,7 +376,8 @@ func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, des
githubEnterpriseClient: client,
destinationRepositoryOwner: destinationRepositoryOwner,
destinationRepositoryName: destinationRepositoryName,
destinationToken: destinationToken,
destinationToken: &token,
actionsAdminUser: actionsAdminUser,
force: force,
pushSSH: pushSSH,
}
Expand Down
4 changes: 3 additions & 1 deletion internal/push/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/go-git/go-git/v5"
"github.com/gorilla/mux"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"

"github.com/google/go-github/v32/github"
)
Expand All @@ -29,13 +30,14 @@ func getTestPushService(t *testing.T, cacheDirectoryString string, githubEnterpr
} else {
githubEnterpriseClient = nil
}
token := oauth2.Token{AccessToken: "token"}
return pushService{
ctx: context.Background(),
cacheDirectory: cacheDirectory,
githubEnterpriseClient: githubEnterpriseClient,
destinationRepositoryOwner: "destination-repository-owner",
destinationRepositoryName: "destination-repository-name",
destinationToken: "token",
destinationToken: &token,
}
}

Expand Down

0 comments on commit 9436204

Please sign in to comment.