Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ RUN apk add --update --no-cache \
COPY scripts/askpass.sh /usr/local/bin/askpass.sh
COPY --from=builder /go/src/github.com/telia-oss/github-pr-resource/build /opt/resource
RUN chmod +x /opt/resource/*

ENV GITHUB_APP_CRED_HELPER_VERSION="v0.3.3"
ENV BIN_PATH_TARGET=/usr/local/bin
RUN curl -L https://github.com/bdellegrazie/git-credential-github-app/releases/download/${GITHUB_APP_CRED_HELPER_VERSION}/git-credential-github-app_${GITHUB_APP_CRED_HELPER_VERSION}_Linux_x86_64.tar.gz | tar zxv -C ${BIN_PATH_TARGET}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding such external component should be justified in the PR description.

Plus, why not use the latest v0.3.3 version?

Which raises the question of how we shall bump this new dependency?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it to the latest version 9f4ae0f

I am open to suggestions for how to keep it up to date. with say, the git resource, we have to build our own version of the git resource to add support for github app auth, since the regular git resource is agnostic to providers. but supporting github apps in this resource seems highly desirable

COPY --from=builder /go/src/github.com/telia-oss/github-pr-resource/build /opt/resource


FROM resource
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,16 @@ automated tests.
| `required_review_approvals` | No | `2` | Disable triggering of the resource if the pull request does not have at least `X` approved review(s). |
| `trusted_teams` | No | `["wg-cf-on-k8s-bots"]` | PRs from members of the trusted teams always trigger the resource regardless of the PR approval status. |
| `trusted_users` | No | `["dependabot"]` | PRs from trusted users always trigger the resource regardless of the PR approval status. |
| `git_crypt_key` | No | `AEdJVENSWVBUS0VZAAAAA...` | Base64 encoded git-crypt key. Setting this will unlock / decrypt the repository with git-crypt. To get the key simply execute `git-crypt export-key -- - | base64` in an encrypted repository. |
| `git_crypt_key` | No | `AEdJVENSWVBUS0VZAAAAA...` | Base64 encoded git-crypt key. Setting this will unlock / decrypt the repository with git-crypt. To get the key simply execute `git-crypt export-key -- - | base64` in an encrypted repository. |
| `base_branch` | No | `master` | Name of a branch. The pipeline will only trigger on pull requests against the specified branch. |
| `labels` | No | `["bug", "enhancement"]` | The labels on the PR. The pipeline will only trigger on pull requests having at least one of the specified labels. |
| `disable_git_lfs` | No | `true` | Disable Git LFS, skipping an attempt to convert pointers of files tracked into their corresponding objects when checked out into a working copy. |
| `states` | No | `["OPEN", "MERGED"]` | The PR states to select (`OPEN`, `MERGED` or `CLOSED`). The pipeline will only trigger on pull requests matching one of the specified states. Default is ["OPEN"]. |
| `use_github_app` | No | `false` | Whether to authenticate using a github app or not. |
| `github_organization` | No | `Vault-tec` | Which Github organization your github app is in. |
| `private_key` | No | `-----BEGIN RSA...` | Private key for your github app. |
| `installation_id` | No | `12356` | Installation id for your github app. |
| `application_id` | No | `12356` | Application id for your github app. |

**Notes:**

Expand Down
56 changes: 45 additions & 11 deletions git.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,25 @@ func NewGitClient(source *Source, dir string, output io.Writer) (*GitClient, err
os.Setenv("GIT_LFS_SKIP_SMUDGE", "true")
}
return &GitClient{
AccessToken: source.AccessToken,
Directory: dir,
Output: output,
AccessToken: source.AccessToken,
PrivateKey: source.PrivateKey,
UseGithubApp: source.UseGitHubApp,
ApplicationID: source.ApplicationID,
GithubOrganization: source.GithubOrganization,
Directory: dir,
Output: output,
}, nil
}

// GitClient ...
type GitClient struct {
AccessToken string
Directory string
Output io.Writer
AccessToken string
UseGithubApp bool
Directory string
ApplicationID int64
GithubOrganization string
PrivateKey string
Output io.Writer
}

func (g *GitClient) command(name string, arg ...string) *exec.Cmd {
Expand All @@ -55,9 +63,15 @@ func (g *GitClient) command(name string, arg ...string) *exec.Cmd {
cmd.Stdout = g.Output
cmd.Stderr = g.Output
cmd.Env = os.Environ()
if !g.UseGithubApp {
cmd.Env = append(cmd.Env,
"X_OAUTH_BASIC_TOKEN="+g.AccessToken)
}

cmd.Env = append(cmd.Env,
"X_OAUTH_BASIC_TOKEN="+g.AccessToken,
"GIT_ASKPASS=/usr/local/bin/askpass.sh")
fmt.Fprint(os.Stderr, fmt.Sprintf("\n%s %v", name, arg))

return cmd
}

Expand All @@ -75,12 +89,29 @@ func (g *GitClient) Init(branch string) error {
if err := g.command("git", "config", "--global", "user.email", "concourse@local").Run(); err != nil {
return fmt.Errorf("failed to configure git email: %s", err)
}
if err := g.command("git", "config", "--global", "url.https://[email protected]/.insteadOf", "[email protected]:").Run(); err != nil {
return fmt.Errorf("failed to configure github url: %s", err)
}
if err := g.command("git", "config", "--global", "url.https://.insteadOf", "git://").Run(); err != nil {
return fmt.Errorf("failed to configure github url: %s", err)
}
if !g.UseGithubApp {
if err := g.command("git", "config", "url.https://[email protected]/.insteadOf", "[email protected]:").Run(); err != nil {
return fmt.Errorf("failed to configure github url: %s", err)
}
} else {
err := ioutil.WriteFile(filePath, []byte(g.PrivateKey), 0600)
if err != nil {
fmt.Println("Error writing private key:", err)
os.Exit(1)
}

helperStr := fmt.Sprintf("!git-credential-github-app --appId %d -organization %s -username x-access-token -privateKeyFile /tmp/git-resource-private-key", g.ApplicationID, g.GithubOrganization)
if err := g.command("git", "config", "credential.https://github.com.helper", helperStr).Run(); err != nil {
return fmt.Errorf("failed to configure github url: %s", err)
}
} else {
if err := g.command("git", "config", "url.https://[email protected]/.insteadOf", "[email protected]:").Run(); err != nil {
return fmt.Errorf("failed to configure github url: %s", err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smaller branch of if-then-else should come first: please invert the condition.

Copy link
Author

@scottillogical scottillogical Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
return nil
}

Expand Down Expand Up @@ -232,6 +263,9 @@ func (g *GitClient) Endpoint(uri string) (string, error) {
if err != nil {
return "", fmt.Errorf("failed to parse commit url: %s", err)
}
endpoint.User = url.UserPassword("x-oauth-basic", g.AccessToken)
if !g.UseGithubApp {
endpoint.User = url.UserPassword("x-oauth-basic", g.AccessToken)
}

return endpoint.String(), nil
}
32 changes: 24 additions & 8 deletions github.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strconv"
"strings"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v61/github"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
Expand Down Expand Up @@ -46,23 +47,38 @@ func NewGithubClient(s *Source) (*GithubClient, error) {
return nil, err
}

// We need a transport that we can update if using GitHub App authentication
transport := http.DefaultTransport.(*http.Transport).Clone()

// Skip SSL verification for self-signed certificates
// source: https://github.com/google/go-github/pull/598#issuecomment-333039238
var ctx context.Context
if s.SkipSSLVerification {
insecureClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
insecureClient := &http.Client{Transport: transport}
ctx = context.WithValue(context.TODO(), oauth2.HTTPClient, insecureClient)
} else {
ctx = context.TODO()
}

client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: s.AccessToken},
))
var client *http.Client
if !s.UseGitHubApp {
// Client using oauth2 wrapper
client = oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: s.AccessToken},
))
} else {
var ghAppInstallationTransport *ghinstallation.Transport
ghAppInstallationTransport, err = ghinstallation.New(transport, s.ApplicationID, s.InstallationID, []byte(s.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to generate application installation access token using private key: %s", err)
}

// Client using ghinstallation transport
client = &http.Client{
Transport: ghAppInstallationTransport,
}
}

var v3 *github.Client
if s.V3Endpoint != "" {
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.23.0
toolchain go1.23.7

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0
github.com/google/go-github/v61 v61.0.0
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
Expand All @@ -14,6 +15,8 @@ require (

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/google/go-github/v69 v69.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0 h1:0D4vKCHOvYrDU8u61TnE2JfNT4VRrBLphmxtqazTO+M=
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0/go.mod h1:LOVmdZYVZ8jqdr4n9wWm1ocDiMz9IfMGfRkaYC1a52A=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
github.com/google/go-github/v69 v69.0.0 h1:YnFvZ3pEIZF8KHmI8xyQQe3mYACdkhnaTV2hr7CP2/w=
github.com/google/go-github/v69 v69.0.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
23 changes: 21 additions & 2 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,31 @@ type Source struct {
States []githubv4.PullRequestState `json:"states"`
TrustedTeams []string `json:"trusted_teams"`
TrustedUsers []string `json:"trusted_users"`
UseGitHubApp bool `json:"use_github_app"`
GithubOrganization string `json:"github_organization"`
PrivateKey string `json:"private_key"`
ApplicationID int64 `json:"application_id"`
InstallationID int64 `json:"installation_id"`
}

// Validate the source configuration.
func (s *Source) Validate() error {
if s.AccessToken == "" {
return errors.New("access_token must be set")
if s.AccessToken == "" && !s.UseGitHubApp {
return errors.New("access_token must be set if not using GitHub App authentication")
}
if s.UseGitHubApp {
if s.PrivateKey == "" {
return errors.New("private_key is required for GitHub App authentication")
}
if s.ApplicationID == 0 || s.InstallationID == 0 {
return errors.New("application_id and installation_id must be set if using GitHub App authentication")
}
if len(s.GithubOrganization) == 0 {
return errors.New("github_organization must be set if using GitHub App authentication")
}
if s.AccessToken != "" {
return errors.New("access_token is not required when using GitHub App authentication")
}
}
if s.Repository == "" {
return errors.New("repository must be set")
Expand Down
120 changes: 120 additions & 0 deletions models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package resource_test

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
resource "github.com/telia-oss/github-pr-resource"
)

func TestSource(t *testing.T) {
tests := []struct {
description string
source resource.Source
wantErr string
}{
{
description: "validate passes",
source: resource.Source{
AccessToken: "123456",
Repository: "test/test",
},
},
{
description: "should have an access_token",
source: resource.Source{
Repository: "test/test",
},
wantErr: "access_token must be set if not using GitHub App authentication",
},
{
description: "should have a repository",
source: resource.Source{
AccessToken: "123456",
},
wantErr: "repository must be set",
},
{
description: "should support GitHub App authentication",
source: resource.Source{
Repository: "test/test",
GithubOrganization: "test",
UseGitHubApp: true,
PrivateKey: "key.pem",
ApplicationID: 123456,
InstallationID: 1,
},
},
{
description: "private_key App configuration values",
source: resource.Source{
Repository: "test/test",
UseGitHubApp: true,
ApplicationID: 123456,
InstallationID: 1,
},
wantErr: "private_key is required for GitHub App authentication",
},
{
description: "requires an application_id and installation_id GitHub App configuration values",
source: resource.Source{
Repository: "test/test",
UseGitHubApp: true,
PrivateKey: "key.pem",
ApplicationID: 123456,
},
wantErr: "application_id and installation_id must be set if using GitHub App authentication",
},
{
description: "should not have an access_token when using GitHub App authentication",
source: resource.Source{
Repository: "test/test",
UseGitHubApp: true,
GithubOrganization: "test",
PrivateKey: "key.pem",
ApplicationID: 123456,
InstallationID: 1,
AccessToken: "123456",
},
wantErr: "access_token is not required when using GitHub App authentication",
},
{
description: "requires v3_endpoint when v4_endpoint is set",
source: resource.Source{
AccessToken: "123456",
Repository: "test/test",
V3Endpoint: "https://github.com/v3",
},
wantErr: "v4_endpoint must be set together with v3_endpoint",
},
{
description: "requires v4_endpoint when v3_endpoint is set",
source: resource.Source{
AccessToken: "123456",
Repository: "test/test",
V4Endpoint: "https://github.com/v4",
},
wantErr: "v3_endpoint must be set together with v4_endpoint",
},
}

for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
err := tc.source.Validate()

if tc.wantErr != "" {
if err == nil {
t.Logf("Expected error '%s', got nothing", tc.wantErr)
t.Fail()
}
assert.EqualError(t, err, tc.wantErr, fmt.Sprintf("Expected '%s', got '%s'", tc.wantErr, err))
}

if tc.wantErr == "" && err != nil {
t.Logf("Got an error when none expected: %s", err)
t.Fail()
}
})
}
}