Jelajahi Sumber

create preview actions file

Alexander Belanger 4 tahun lalu
induk
melakukan
a32d86389e

+ 42 - 7
api/server/handlers/environment/create.go

@@ -5,12 +5,14 @@ import (
 	"strconv"
 
 	"github.com/bradleyfalzon/ghinstallation"
-	"github.com/google/go-github/github"
+	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
 )
@@ -31,6 +33,7 @@ func NewCreateEnvironmentHandler(
 
 func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
@@ -55,13 +58,45 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	// TODO: upon environment creation, write Github actions files to the repo
-	// client, err := getGithubClientFromEnvironment(c.Config(), env)
+	// write Github actions files to the repo
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
-	// if err != nil {
-	// 	c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-	// 	return
-	// }
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// generate porter jwt token
+	jwt, err := token.GetTokenForAPI(user.ID, project.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	_, err = actions.SetupEnv(&actions.EnvOpts{
+		Client:            client,
+		ServerURL:         c.Config().ServerConf.ServerURL,
+		PorterToken:       encoded,
+		GitRepoOwner:      request.GitRepoOwner,
+		GitRepoName:       request.GitRepoName,
+		ProjectID:         project.ID,
+		ClusterID:         cluster.ID,
+		GitInstallationID: uint(ga.InstallationID),
+		EnvironmentName:   request.Name,
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
 
 	c.WriteResult(w, r, env.ToEnvironmentType())
 }

+ 23 - 6
api/server/handlers/environment/delete.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
 )
@@ -45,13 +46,29 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	// TODO: delete corresponding Github actions files using the client
-	// client, err := getGithubClientFromEnvironment(c.Config(), env)
+	// delete Github actions files from the repo
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
-	// if err != nil {
-	// 	c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-	// 	return
-	// }
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	err = actions.DeleteEnv(&actions.EnvOpts{
+		Client:            client,
+		ServerURL:         c.Config().ServerConf.ServerURL,
+		GitRepoOwner:      env.GitRepoOwner,
+		GitRepoName:       env.GitRepoName,
+		ProjectID:         project.ID,
+		ClusterID:         cluster.ID,
+		GitInstallationID: uint(ga.InstallationID),
+		EnvironmentName:   env.Name,
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
 
 	// delete the environment
 	env, err = c.Repo().Environment().DeleteEnvironment(env)

+ 1 - 1
api/server/handlers/environment/finalize_deployment.go

@@ -4,7 +4,7 @@ import (
 	"context"
 	"net/http"
 
-	"github.com/google/go-github/github"
+	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"

+ 2 - 3
go.mod

@@ -26,8 +26,7 @@ require (
 	github.com/go-test/deep v1.0.7
 	github.com/google/go-github v17.0.0+incompatible
 	github.com/google/go-github/v29 v29.0.3 // indirect
-	github.com/google/go-github/v33 v33.0.0
-	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/google/go-github/v41 v41.0.0
 	github.com/gorilla/schema v1.2.0
 	github.com/gorilla/securecookie v1.1.1
 	github.com/gorilla/sessions v1.2.1
@@ -54,7 +53,7 @@ require (
 	github.com/spf13/viper v1.8.1
 	github.com/stretchr/testify v1.7.0
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
-	golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
+	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
 	golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
 	google.golang.org/api v0.44.0
 	google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c

+ 5 - 15
go.sum

@@ -138,7 +138,6 @@ github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqR
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
 github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
-github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
@@ -164,9 +163,7 @@ github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDw
 github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
 github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
 github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
-github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
 github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
-github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@@ -652,8 +649,8 @@ github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD
 github.com/google/go-github/v29 v29.0.3 h1:IktKCTwU//aFHnpA+2SLIi7Oo9uhAzgsdZNbcAqhgdc=
 github.com/google/go-github/v29 v29.0.3/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
 github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
-github.com/google/go-github/v33 v33.0.0 h1:qAf9yP0qc54ufQxzwv+u9H0tiVOnPJxo0lI/JXqw3ZM=
-github.com/google/go-github/v33 v33.0.0/go.mod h1:GMdDnVZY/2TsWgp/lkYnpSAh6TrzhANBBwm6k6TTEXg=
+github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
+github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
@@ -748,22 +745,18 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
 github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
 github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 h1:PFfGModn55JA0oBsvFghhj0v93me+Ctr3uHC/UmFAls=
 github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hashicorp/terraform-exec v0.15.0 h1:cqjh4d8HYNQrDoEmlSGelHmg2DYDh5yayckvJ5bV18E=
 github.com/hashicorp/terraform-exec v0.15.0/go.mod h1:H4IG8ZxanU+NW0ZpDRNsvh9f0ul7C0nHP+rUR/CHs7I=
-github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY=
 github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk=
 github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
 github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0=
@@ -1171,10 +1164,6 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
 github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/porter-dev/switchboard v0.0.0-20211203120508-691813a27c1b h1:xcM7l5Bzhi4xQsMEGr5MELPWMesVBMQdMqD6AUh5wiw=
-github.com/porter-dev/switchboard v0.0.0-20211203120508-691813a27c1b/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
-github.com/porter-dev/switchboard v0.0.0-20211206152649-b6792e4dc331 h1:winqj0G5++gjklyo/lZtxqEMtQbj2eiIrwOzEESaFGI=
-github.com/porter-dev/switchboard v0.0.0-20211206152649-b6792e4dc331/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/porter-dev/switchboard v0.0.0-20211208133739-316acab6516d h1:IKyljDrQPhCOzfnmp5r9KeY67X7KQo70aH8NNEfSomY=
 github.com/porter-dev/switchboard v0.0.0-20211208133739-316acab6516d/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -1413,7 +1402,6 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1
 github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
 github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
 github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
-github.com/zclconf/go-cty v1.9.1 h1:viqrgQwFl5UpSxc046qblj78wZXVDFnSOufaOTER+cc=
 github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@@ -1500,8 +1488,9 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1728,6 +1717,7 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c h1:Lyn7+CqXIiC+LOR9aHD6jDK+hPcmAuCfuXztd1v4w1Q=

+ 48 - 37
internal/integrations/ci/actions/actions.go

@@ -8,7 +8,7 @@ import (
 
 	"github.com/Masterminds/semver/v3"
 	"github.com/bradleyfalzon/ghinstallation"
-	"github.com/google/go-github/v33/github"
+	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
@@ -78,7 +78,7 @@ func (g *GithubActions) Setup() ([]byte, error) {
 
 	if !g.DryRun {
 		// create porter token secret
-		if err := g.createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken); err != nil {
+		if err := createGithubSecret(client, getPorterTokenSecretName(g.ProjectID), g.PorterToken, g.GitRepoOwner, g.GitRepoName); err != nil {
 			return nil, err
 		}
 	}
@@ -90,7 +90,15 @@ func (g *GithubActions) Setup() ([]byte, error) {
 	}
 
 	if !g.DryRun && g.ShouldCreateWorkflow {
-		_, err = g.commitGithubFile(client, g.getPorterYMLFileName(), workflowYAML)
+		branch := g.GitBranch
+
+		if branch == "" {
+			branch = g.defaultBranch
+		}
+
+		isOAuth := g.GithubOAuthIntegration != nil
+
+		_, err = commitGithubFile(client, g.getPorterYMLFileName(), workflowYAML, g.GitRepoOwner, g.GitRepoName, branch, isOAuth)
 		if err != nil {
 			return workflowYAML, err
 		}
@@ -136,7 +144,15 @@ func (g *GithubActions) Cleanup() error {
 		}
 	}
 
-	return g.deleteGithubFile(client, g.getPorterYMLFileName())
+	branch := g.GitBranch
+
+	if branch == "" {
+		branch = g.defaultBranch
+	}
+
+	isOAuth := g.GithubOAuthIntegration != nil
+
+	return deleteGithubFile(client, g.getPorterYMLFileName(), g.GitRepoOwner, g.GitRepoName, branch, isOAuth)
 }
 
 type GithubActionYAMLStep struct {
@@ -163,7 +179,7 @@ type GithubActionYAMLJob struct {
 }
 
 type GithubActionYAML struct {
-	On GithubActionYAMLOnPush `yaml:"on,omitempty"`
+	On interface{} `yaml:"on,omitempty"`
 
 	Name string `yaml:"name,omitempty"`
 
@@ -174,7 +190,7 @@ func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 	gaSteps := []GithubActionYAMLStep{
 		getCheckoutCodeStep(),
 		getSetTagStep(),
-		getUpdateAppStep(g.ServerURL, g.getPorterTokenSecretName(), g.ProjectID, g.ClusterID, g.ReleaseName, g.ReleaseNamespace, g.Version),
+		getUpdateAppStep(g.ServerURL, getPorterTokenSecretName(g.ProjectID), g.ProjectID, g.ClusterID, g.ReleaseName, g.ReleaseNamespace, g.Version),
 	}
 
 	branch := g.GitBranch
@@ -245,13 +261,15 @@ func (g *GithubActions) getClient() (*github.Client, error) {
 	return github.NewClient(&http.Client{Transport: itr}), nil
 }
 
-func (g *GithubActions) createGithubSecret(
+func createGithubSecret(
 	client *github.Client,
 	secretName,
-	secretValue string,
+	secretValue,
+	gitRepoOwner,
+	gitRepoName string,
 ) error {
 	// get the public key for the repo
-	key, _, err := client.Actions.GetRepoPublicKey(context.TODO(), g.GitRepoOwner, g.GitRepoName)
+	key, _, err := client.Actions.GetRepoPublicKey(context.TODO(), gitRepoOwner, gitRepoName)
 
 	if err != nil {
 		return err
@@ -283,7 +301,7 @@ func (g *GithubActions) createGithubSecret(
 	}
 
 	// write the secret to the repo
-	_, err = client.Actions.CreateOrUpdateRepoSecret(context.TODO(), g.GitRepoOwner, g.GitRepoName, encryptedSecret)
+	_, err = client.Actions.CreateOrUpdateRepoSecret(context.TODO(), gitRepoOwner, gitRepoName, encryptedSecret)
 
 	return err
 }
@@ -323,7 +341,7 @@ func (g *GithubActions) createEnvSecret(client *github.Client) error {
 
 	secretName := g.getBuildEnvSecretName()
 
-	return g.createGithubSecret(client, secretName, strings.Join(lines, "\n"))
+	return createGithubSecret(client, secretName, strings.Join(lines, "\n"), g.GitRepoOwner, g.GitRepoName)
 }
 
 func (g *GithubActions) getWebhookSecretName() string {
@@ -344,29 +362,25 @@ func (g *GithubActions) getPorterYMLFileName() string {
 	)
 }
 
-func (g *GithubActions) getPorterTokenSecretName() string {
-	return fmt.Sprintf("PORTER_TOKEN_%d", g.ProjectID)
+func getPorterTokenSecretName(projID uint) string {
+	return fmt.Sprintf("PORTER_TOKEN_%d", projID)
 }
 
-func (g *GithubActions) commitGithubFile(
+func commitGithubFile(
 	client *github.Client,
 	filename string,
 	contents []byte,
+	gitRepoOwner, gitRepoName, branch string,
+	isOAuth bool,
 ) (string, error) {
 	filepath := ".github/workflows/" + filename
 	sha := ""
 
-	branch := g.GitBranch
-
-	if branch == "" {
-		branch = g.defaultBranch
-	}
-
 	// get contents of a file if it exists
 	fileData, _, _, _ := client.Repositories.GetContents(
 		context.TODO(),
-		g.GitRepoOwner,
-		g.GitRepoName,
+		gitRepoOwner,
+		gitRepoName,
 		filepath,
 		&github.RepositoryContentGetOptions{
 			Ref: branch,
@@ -384,7 +398,7 @@ func (g *GithubActions) commitGithubFile(
 		SHA:     &sha,
 	}
 
-	if g.GithubOAuthIntegration != nil {
+	if isOAuth {
 		opts.Committer = &github.CommitAuthor{
 			Name:  github.String("Porter Bot"),
 			Email: github.String("contact@getporter.dev"),
@@ -393,8 +407,8 @@ func (g *GithubActions) commitGithubFile(
 
 	resp, _, err := client.Repositories.UpdateFile(
 		context.TODO(),
-		g.GitRepoOwner,
-		g.GitRepoName,
+		gitRepoOwner,
+		gitRepoName,
 		filepath,
 		opts,
 	)
@@ -406,21 +420,18 @@ func (g *GithubActions) commitGithubFile(
 	return *resp.Commit.SHA, nil
 }
 
-func (g *GithubActions) deleteGithubFile(
+func deleteGithubFile(
 	client *github.Client,
-	filename string,
+	filename, gitRepoOwner, gitRepoName, branch string,
+	isOAuth bool,
 ) error {
-	branch := g.GitBranch
-	if branch == "" {
-		branch = g.defaultBranch
-	}
-
 	filepath := ".github/workflows/" + filename
+
 	// get contents of a file if it exists
 	fileData, _, _, _ := client.Repositories.GetContents(
 		context.TODO(),
-		g.GitRepoOwner,
-		g.GitRepoName,
+		gitRepoOwner,
+		gitRepoName,
 		filepath,
 		&github.RepositoryContentGetOptions{
 			Ref: branch,
@@ -438,7 +449,7 @@ func (g *GithubActions) deleteGithubFile(
 		SHA:     &sha,
 	}
 
-	if g.GithubOAuthIntegration != nil {
+	if isOAuth {
 		opts.Committer = &github.CommitAuthor{
 			Name:  github.String("Porter Bot"),
 			Email: github.String("contact@getporter.dev"),
@@ -447,8 +458,8 @@ func (g *GithubActions) deleteGithubFile(
 
 	_, _, err := client.Repositories.DeleteFile(
 		context.TODO(),
-		g.GitRepoOwner,
-		g.GitRepoName,
+		gitRepoOwner,
+		gitRepoName,
 		filepath,
 		opts,
 	)

+ 147 - 0
internal/integrations/ci/actions/preview.go

@@ -0,0 +1,147 @@
+package actions
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/google/go-github/v41/github"
+
+	"gopkg.in/yaml.v2"
+)
+
+type EnvOpts struct {
+	Client                                  *github.Client
+	ServerURL                               string
+	PorterToken                             string
+	GitRepoOwner, GitRepoName               string
+	EnvironmentName                         string
+	ProjectID, ClusterID, GitInstallationID uint
+}
+
+func SetupEnv(opts *EnvOpts) ([]byte, error) {
+	// create Github environment if it does not exist
+	// _, resp, err := opts.Client.Repositories.GetEnvironment(
+	// 	context.Background(),
+	// 	opts.GitRepoOwner,
+	// 	opts.GitRepoName,
+	// 	opts.EnvironmentName,
+	// )
+
+	// if resp.StatusCode == http.StatusNotFound {
+	// 	_, _, err := opts.Client.Repositories.CreateUpdateEnvironment(
+	// 		context.Background(),
+	// 		opts.GitRepoOwner,
+	// 		opts.GitRepoName,
+	// 		opts.EnvironmentName,
+	// 		&github.CreateUpdateEnvironment{},
+	// 	)
+
+	// 	if err != nil {
+	// 		return nil, err
+	// 	}
+	// } else if err != nil {
+	// 	return nil, err
+	// }
+
+	// create porter token secret
+	err := createGithubSecret(
+		opts.Client,
+		getPorterTokenSecretName(opts.ProjectID),
+		opts.PorterToken,
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// get the repository to find the default branch
+	repo, _, err := opts.Client.Repositories.Get(
+		context.TODO(),
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	defaultBranch := repo.GetDefaultBranch()
+
+	workflowYAML, err := getPreviewActionYAML(opts)
+
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = commitGithubFile(
+		opts.Client,
+		fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
+		workflowYAML,
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+		defaultBranch,
+		false,
+	)
+
+	if err != nil {
+		return workflowYAML, err
+	}
+
+	return workflowYAML, err
+}
+
+func DeleteEnv(opts *EnvOpts) error {
+	// get the repository to find the default branch
+	repo, _, err := opts.Client.Repositories.Get(
+		context.TODO(),
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	defaultBranch := repo.GetDefaultBranch()
+
+	return deleteGithubFile(
+		opts.Client,
+		fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+		defaultBranch,
+		false,
+	)
+}
+
+func getPreviewActionYAML(opts *EnvOpts) ([]byte, error) {
+	gaSteps := []GithubActionYAMLStep{
+		getCheckoutCodeStep(),
+		getCreatePreviewEnvStep(
+			opts.ServerURL,
+			getPorterTokenSecretName(opts.ProjectID),
+			opts.ProjectID,
+			opts.ClusterID,
+			opts.GitInstallationID,
+			opts.GitRepoName,
+			// TODO: change to actual release version
+			"master",
+		),
+	}
+
+	actionYAML := GithubActionYAML{
+		On:   []string{"pull_request"},
+		Name: "Porter Preview Environment",
+		Jobs: map[string]GithubActionYAMLJob{
+			"porter-preview": {
+				RunsOn: "ubuntu-latest",
+				Steps:  gaSteps,
+			},
+		},
+	}
+
+	return yaml.Marshal(actionYAML)
+}

+ 18 - 0
internal/integrations/ci/actions/steps.go

@@ -5,6 +5,7 @@ import (
 )
 
 const updateAppActionName = "porter-dev/porter-update-action"
+const createPreviewActionName = "porter-dev/porter-preview-action"
 
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
@@ -37,3 +38,20 @@ func getUpdateAppStep(serverURL, porterTokenSecretName string, projectID uint, c
 		Timeout: 20,
 	}
 }
+
+func getCreatePreviewEnvStep(serverURL, porterTokenSecretName string, projectID, clusterID, gitInstallationID uint, repoName, actionVersion string) GithubActionYAMLStep {
+	return GithubActionYAMLStep{
+		Name: "Create Porter preview env",
+		Uses: fmt.Sprintf("%s@%s", createPreviewActionName, actionVersion),
+		With: map[string]string{
+			"cluster":         fmt.Sprintf("%d", clusterID),
+			"host":            serverURL,
+			"project":         fmt.Sprintf("%d", projectID),
+			"token":           fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
+			"namespace":       fmt.Sprintf("pr-${{ github.event.pull_request.number }}-%s", repoName),
+			"pr_id":           "${{ github.event.pull_request.number }}",
+			"installation_id": fmt.Sprintf("%d", gitInstallationID),
+		},
+		Timeout: 30,
+	}
+}