Changes necessary to run with the Go 1.11 App Engine runtime.

This commit is contained in:
Mihai Parparita 2020-03-05 21:30:47 -08:00
parent eb7efc60bc
commit 6c662e2109
14 changed files with 118 additions and 99 deletions

View file

@ -1,6 +1,6 @@
# RetroGit
Service that shows you your GitHub commits from a year ago. Includes a mail digest to that you can see each day what you were up to in the past.
Service that shows you your GitHub commits from previous years. Includes a mail digest to that you can see each day what you were up to in the past.
It's currently running at [https://www.retrogit.com/](https://www.retrogit.com/).
@ -21,5 +21,5 @@ The server can the be accessed at [http://localhost:8080/](http://localhost:8080
## Deploying to App Engine
```
gcloud app deploy --project retrogit app/app.yaml
./deploy.sh
```

View file

@ -1,13 +1,13 @@
package retrogit
package main
import (
"bytes"
"context"
"encoding/gob"
"errors"
"time"
"appengine"
"appengine/datastore"
"google.golang.org/appengine/datastore"
"code.google.com/p/goauth2/oauth"
"github.com/google/go-github/github"
@ -28,7 +28,7 @@ type Account struct {
WeeklyDay time.Weekday
}
func getAccount(c appengine.Context, githubUserId int) (*Account, error) {
func getAccount(c context.Context, githubUserId int) (*Account, error) {
key := datastore.NewKey(c, "Account", "", int64(githubUserId), nil)
account := new(Account)
err := datastore.Get(c, key, account)
@ -63,7 +63,7 @@ func initAccount(account *Account) error {
return nil
}
func getAllAccounts(c appengine.Context) ([]Account, error) {
func getAllAccounts(c context.Context) ([]Account, error) {
q := datastore.NewQuery("Account")
var accounts []Account
_, err := q.GetAll(c, &accounts)
@ -88,7 +88,7 @@ func (account *Account) IsRepoIdExcluded(repoId int) bool {
return false
}
func (account *Account) Put(c appengine.Context) error {
func (account *Account) Put(c context.Context) error {
w := new(bytes.Buffer)
err := gob.NewEncoder(w).Encode(&account.OAuthToken)
if err != nil {
@ -100,7 +100,7 @@ func (account *Account) Put(c appengine.Context) error {
return err
}
func (account *Account) Delete(c appengine.Context) error {
func (account *Account) Delete(c context.Context) error {
key := datastore.NewKey(c, "Account", "", int64(account.GitHubUserId), nil)
err := datastore.Delete(c, key)
return err

View file

@ -1,11 +1,11 @@
package retrogit
package main
import (
"net/http"
"sort"
"strconv"
"appengine"
"google.golang.org/appengine"
"github.com/google/go-github/github"
)

View file

@ -1,4 +1,4 @@
package retrogit
package main
import (
"encoding/json"
@ -6,13 +6,14 @@ import (
"fmt"
"html/template"
"io/ioutil"
"log"
log_ "log"
"net/http"
"path/filepath"
"strings"
"appengine"
"appengine/mail"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
"google.golang.org/appengine/mail"
"github.com/google/go-github/github"
"github.com/gorilla/sessions"
@ -208,14 +209,14 @@ func handleAppError(e *AppError, w http.ResponseWriter, r *http.Request) {
return
}
} else {
c.Errorf("GitHub fetch error was not of type github.ErrorResponse")
log.Errorf(c, "GitHub fetch error was not of type github.ErrorResponse")
}
} else if e.Type == AppErrorTypeRedirect {
http.Redirect(w, r, e.Message, e.Code)
return
}
if e.Type != AppErrorTypeBadInput {
c.Errorf("%v", e.Error)
log.Errorf(c, "%v", e.Error)
if !appengine.IsDevAppServer() {
sendAppErrorMail(e, r)
}
@ -226,11 +227,11 @@ func handleAppError(e *AppError, w http.ResponseWriter, r *http.Request) {
w.WriteHeader(e.Code)
templateError := templates["internal-error"].Render(w, data)
if templateError != nil {
c.Errorf("Error %s rendering error template.", templateError.Error.Error())
log.Errorf(c, "Error %s rendering error template.", templateError.Error.Error())
}
return
} else {
c.Infof("%v", e.Error)
log.Infof(c, "%v", e.Error)
}
http.Error(w, e.Message, e.Code)
}
@ -260,7 +261,7 @@ Error: %s`,
c := appengine.NewContext(r)
err := mail.Send(c, errorMessage)
if err != nil {
c.Errorf("Error %s sending error email.", err.Error())
log.Errorf(c, "Error %s sending error email.", err.Error())
}
}
@ -323,11 +324,11 @@ func loadTemplates() (templates map[string]*Template) {
}
sharedFileNames, err := filepath.Glob("templates/shared/*.html")
if err != nil {
log.Panicf("Could not read shared template file names %s", err.Error())
log_.Panicf("Could not read shared template file names %s", err.Error())
}
templateFileNames, err := filepath.Glob("templates/*.html")
if err != nil {
log.Panicf("Could not read template file names %s", err.Error())
log_.Panicf("Could not read template file names %s", err.Error())
}
templates = make(map[string]*Template)
for _, templateFileName := range templateFileNames {
@ -344,7 +345,7 @@ func loadTemplates() (templates map[string]*Template) {
_, templateFileName = filepath.Split(fileNames[0])
parsedTemplate, err := template.New(templateFileName).Funcs(funcMap).ParseFiles(fileNames...)
if err != nil {
log.Printf("Could not parse template files for %s: %s", templateFileName, err.Error())
log_.Printf("Could not parse template files for %s: %s", templateFileName, err.Error())
}
templates[templateName] = &Template{parsedTemplate}
}
@ -354,13 +355,13 @@ func loadTemplates() (templates map[string]*Template) {
func loadStyles() (result map[string]template.CSS) {
stylesBytes, err := ioutil.ReadFile("config/styles.json")
if err != nil {
log.Panicf("Could not read styles JSON: %s", err.Error())
log_.Panicf("Could not read styles JSON: %s", err.Error())
}
var stylesJson interface{}
err = json.Unmarshal(stylesBytes, &stylesJson)
result = make(map[string]template.CSS)
if err != nil {
log.Printf("Could not parse styles JSON %s: %s", stylesBytes, err.Error())
log_.Printf("Could not parse styles JSON %s: %s", stylesBytes, err.Error())
return
}
var parse func(string, map[string]interface{}, *string)
@ -377,7 +378,7 @@ func loadStyles() (result map[string]template.CSS) {
parse(path+k, v.(map[string]interface{}), &nestedStyle)
result[path+k] = template.CSS(nestedStyle)
default:
log.Printf("Unexpected type for %s in styles JSON, ignoring", k)
log_.Printf("Unexpected type for %s in styles JSON, ignoring", k)
}
}
}

View file

@ -1,5 +1,4 @@
runtime: go
api_version: go1
runtime: go111
handlers:
- url: /static
@ -11,11 +10,11 @@ handlers:
static_files: static/robots.txt
upload: static/robots.txt
- url: /digest/cron
script: _go_app
script: auto
login: admin
- url: /admin/.*
script: _go_app
script: auto
login: admin
- url: /.*
script: _go_app
script: auto
secure: always

View file

@ -1,8 +1,9 @@
package retrogit
package main
import (
"bufio"
"bytes"
"context"
"crypto/md5"
"fmt"
"io"
@ -11,8 +12,8 @@ import (
"strings"
"time"
"appengine"
"appengine/memcache"
"google.golang.org/appengine/log"
"google.golang.org/appengine/memcache"
)
// Simple http.RoundTripper implementation which wraps an existing transport and
@ -20,7 +21,7 @@ import (
// iteration cycle during development.
type CachingTransport struct {
Transport http.RoundTripper
Context appengine.Context
Context context.Context
}
func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
@ -50,7 +51,7 @@ func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, er
cachedRespItem, err := memcache.Get(t.Context, cacheKey)
if err != nil && err != memcache.ErrCacheMiss {
t.Context.Errorf("Error getting cached response: %v", err)
log.Errorf(t.Context, "Error getting cached response: %v", err)
return t.Transport.RoundTrip(req)
}
if err == nil {
@ -59,17 +60,17 @@ func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, er
if err == nil {
return resp, nil
} else {
t.Context.Errorf("Error readings bytes for cached response: %v", err)
log.Errorf(t.Context, "Error readings bytes for cached response: %v", err)
}
}
t.Context.Infof("Fetching %s", req.URL)
log.Infof(t.Context, "Fetching %s", req.URL)
resp, err = t.Transport.RoundTrip(req)
if err != nil || resp.StatusCode != 200 {
return
}
respBytes, err := httputil.DumpResponse(resp, true)
if err != nil {
t.Context.Errorf("Error dumping bytes for cached response: %v", err)
log.Errorf(t.Context, "Error dumping bytes for cached response: %v", err)
return resp, nil
}
var expiration time.Duration = time.Hour
@ -86,7 +87,7 @@ func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, er
Expiration: expiration,
})
if err != nil {
t.Context.Errorf("Error setting cached response for %s (cache key %s, %d bytes to cache): %v",
log.Errorf(t.Context, "Error setting cached response for %s (cache key %s, %d bytes to cache): %v",
req.URL, cacheKey, len(respBytes), err)
}
return resp, nil

View file

@ -1,13 +1,14 @@
package retrogit
package main
import (
"bytes"
"context"
"fmt"
"sort"
"strings"
"time"
"appengine"
"google.golang.org/appengine/log"
"github.com/google/go-github/github"
)
@ -171,7 +172,7 @@ type Digest struct {
RepoErrors map[string]error
}
func newDigest(c appengine.Context, githubClient *github.Client, account *Account) (*Digest, error) {
func newDigest(c context.Context, githubClient *github.Client, account *Account) (*Digest, error) {
user, _, err := githubClient.Users.Get("")
if err != nil {
return nil, err
@ -225,7 +226,7 @@ func newDigest(c appengine.Context, githubClient *github.Client, account *Accoun
digest.fetch(githubClient)
for repoFullName, err := range digest.RepoErrors {
c.Errorf("Error fetching %s: %s", repoFullName, err.Error())
log.Errorf(c, "Error fetching %s: %s", repoFullName, err.Error())
}
return digest, nil
}

2
app/index.yaml Normal file
View file

@ -0,0 +1,2 @@
indexes:
# AUTOGENERATED

View file

@ -1,4 +1,4 @@
package retrogit
package main
// From Martini's recovery package:
// https://github.com/go-martini/martini/blob/master/recovery.go

View file

@ -1,13 +1,15 @@
package retrogit
package main
import (
"context"
"fmt"
"time"
"appengine"
"appengine/datastore"
"appengine/delay"
"appengine/taskqueue"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/delay"
"google.golang.org/appengine/log"
"google.golang.org/appengine/taskqueue"
"github.com/google/go-github/github"
)
@ -23,16 +25,16 @@ type RepoVintage struct {
Vintage time.Time `datastore:",noindex"`
}
func getVintageKey(c appengine.Context, userId int, repoId int) *datastore.Key {
func getVintageKey(c context.Context, userId int, repoId int) *datastore.Key {
return datastore.NewKey(c, "RepoVintage", fmt.Sprintf("%d-%d", userId, repoId), 0, nil)
}
var computeVintageFunc *delay.Function
func computeVintage(c appengine.Context, userId int, userLogin string, repoId int, repoOwnerLogin string, repoName string) error {
func computeVintage(c context.Context, userId int, userLogin string, repoId int, repoOwnerLogin string, repoName string) error {
account, err := getAccount(c, userId)
if err != nil {
c.Errorf("Could not load account %d: %s. Presumed deleted, aborting computing vintage for %s/%s", userId, err.Error(), repoOwnerLogin, repoName)
log.Errorf(c, "Could not load account %d: %s. Presumed deleted, aborting computing vintage for %s/%s", userId, err.Error(), repoOwnerLogin, repoName)
return nil
}
@ -42,7 +44,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
repo, response, err := githubClient.Repositories.Get(repoOwnerLogin, repoName)
if response.StatusCode == 403 || response.StatusCode == 404 {
c.Warningf("Got a %d when trying to look up %s/%s (%d)", response.StatusCode, repoOwnerLogin, repoName, repoId)
log.Warningf(c, "Got a %d when trying to look up %s/%s (%d)", response.StatusCode, repoOwnerLogin, repoName, repoId)
_, err = datastore.Put(c, getVintageKey(c, userId, repoId), &RepoVintage{
UserId: userId,
RepoId: repoId,
@ -50,7 +52,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
})
return err
} else if err != nil {
c.Errorf("Could not load repo %s/%s (%d): %s", repoOwnerLogin, repoName, repoId, err.Error())
log.Errorf(c, "Could not load repo %s/%s (%d): %s", repoOwnerLogin, repoName, repoId, err.Error())
return err
}
@ -69,7 +71,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
// GitHub returns with a 409 when a repository is empty.
commits = make([]github.RepositoryCommit, 0)
} else if err != nil {
c.Errorf("Could not load commits for repo %s (%d): %s", *repo.FullName, repoId, err.Error())
log.Errorf(c, "Could not load commits for repo %s (%d): %s", *repo.FullName, repoId, err.Error())
return err
}
@ -78,10 +80,10 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
if len(commits) > 0 {
stats, response, err := githubClient.Repositories.ListContributorsStats(repoOwnerLogin, repoName)
if response.StatusCode == 202 {
c.Infof("Stats were not available for %s, will try again later", *repo.FullName)
log.Infof(c, "Stats were not available for %s, will try again later", *repo.FullName)
task, err := computeVintageFunc.Task(userId, userLogin, repoId, repoOwnerLogin, repoName)
if err != nil {
c.Errorf("Could create delayed task for %s: %s", *repo.FullName, err.Error())
log.Errorf(c, "Could create delayed task for %s: %s", *repo.FullName, err.Error())
return err
}
task.Delay = time.Second * 10
@ -89,7 +91,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
return nil
}
if err != nil {
c.Errorf("Could not load stats for repo %s: %s", *repo.FullName, err.Error())
log.Errorf(c, "Could not load stats for repo %s: %s", *repo.FullName, err.Error())
return err
}
for _, stat := range stats {
@ -111,7 +113,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
Vintage: vintage,
})
if err != nil {
c.Errorf("Could save vintage for repo %s: %s", *repo.FullName, err.Error())
log.Errorf(c, "Could save vintage for repo %s: %s", *repo.FullName, err.Error())
return err
}
@ -122,7 +124,7 @@ func init() {
computeVintageFunc = delay.Func("computeVintage", computeVintage)
}
func fillVintages(c appengine.Context, user *github.User, repos []*Repo) error {
func fillVintages(c context.Context, user *github.User, repos []*Repo) error {
if len(repos) > VintageChunkSize {
for chunkStart := 0; chunkStart < len(repos); chunkStart += VintageChunkSize {
chunkEnd := chunkStart + VintageChunkSize
@ -151,7 +153,7 @@ func fillVintages(c appengine.Context, user *github.User, repos []*Repo) error {
if err == datastore.ErrNoSuchEntity {
vintages[i] = nil
} else if err != nil {
c.Errorf("%d/%s vintage fetch error: %s", i, *repos[i].FullName, err.Error())
log.Errorf(c, "%d/%s vintage fetch error: %s", i, *repos[i].FullName, err.Error())
return err
}
}
@ -238,7 +240,7 @@ type UserRepos struct {
Repos []*Repo
}
func getRepos(c appengine.Context, githubClient *github.Client, account *Account, user *github.User) (*Repos, error) {
func getRepos(c context.Context, githubClient *github.Client, account *Account, user *github.User) (*Repos, error) {
clientUserRepos := make([]github.Repository, 0)
page := 1
for {

View file

@ -1,11 +1,12 @@
package retrogit
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
log_ "log"
"net/http"
"net/url"
"strconv"
@ -13,11 +14,12 @@ import (
"sync"
"time"
"appengine"
"appengine/datastore"
"appengine/delay"
"appengine/mail"
"appengine/urlfetch"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/delay"
"google.golang.org/appengine/log"
"google.golang.org/appengine/mail"
"google.golang.org/appengine/urlfetch"
"code.google.com/p/goauth2/oauth"
"github.com/google/go-github/github"
@ -33,7 +35,7 @@ var sessionStore *sessions.CookieStore
var sessionConfig SessionConfig
var templates map[string]*Template
func init() {
func main() {
templates = loadTemplates()
timezones = initTimezones()
sessionStore, sessionConfig = initSession()
@ -62,6 +64,8 @@ func init() {
router.Handle("/admin/repos", AppHandler(reposAdminHandler)).Name("repos-admin")
router.Handle("/admin/delete-account", AppHandler(deleteAccountAdminHandler)).Name("delete-account-admin")
http.Handle("/", router)
appengine.Main()
}
func initGithubOAuthConfig(includePrivateRepos bool) (config oauth.Config) {
@ -72,11 +76,11 @@ func initGithubOAuthConfig(includePrivateRepos bool) (config oauth.Config) {
path += ".json"
configBytes, err := ioutil.ReadFile(path)
if err != nil {
log.Panicf("Could not read GitHub OAuth config from %s: %s", path, err.Error())
log_.Panicf("Could not read GitHub OAuth config from %s: %s", path, err.Error())
}
err = json.Unmarshal(configBytes, &config)
if err != nil {
log.Panicf("Could not parse GitHub OAuth config %s: %s", configBytes, err.Error())
log_.Panicf("Could not parse GitHub OAuth config %s: %s", configBytes, err.Error())
}
repoScopeModifier := ""
if !includePrivateRepos {
@ -232,12 +236,12 @@ func digestCronHandler(w http.ResponseWriter, r *http.Request) *AppError {
if account.Frequency == "weekly" {
now := time.Now().In(account.TimezoneLocation)
if now.Weekday() != account.WeeklyDay {
c.Infof("Skipping %d, since it wants weekly digests on %ss and today is a %s.",
log.Infof(c, "Skipping %d, since it wants weekly digests on %ss and today is a %s.",
account.GitHubUserId, account.WeeklyDay, now.Weekday())
continue
}
}
c.Infof("Enqueing task for %d...", account.GitHubUserId)
log.Infof(c, "Enqueing task for %d...", account.GitHubUserId)
sendDigestForAccountFunc.Call(c, account.GitHubUserId)
}
fmt.Fprint(w, "Done")
@ -246,31 +250,31 @@ func digestCronHandler(w http.ResponseWriter, r *http.Request) *AppError {
var sendDigestForAccountFunc = delay.Func(
"sendDigestForAccount",
func(c appengine.Context, githubUserId int) error {
c.Infof("Sending digest for %d...", githubUserId)
func(c context.Context, githubUserId int) error {
log.Infof(c, "Sending digest for %d...", githubUserId)
account, err := getAccount(c, githubUserId)
if err != nil {
c.Errorf(" Error looking up account: %s", err.Error())
log.Errorf(c, " Error looking up account: %s", err.Error())
return err
}
sent, err := sendDigestForAccount(account, c)
if err != nil {
c.Errorf(" Error: %s", err.Error())
log.Errorf(c, " Error: %s", err.Error())
if !appengine.IsDevAppServer() {
sendDigestErrorMail(err, c, githubUserId)
}
} else if sent {
c.Infof(" Sent!")
log.Infof(c, " Sent!")
} else {
c.Infof(" Not sent, digest was empty")
log.Infof(c, " Not sent, digest was empty")
}
return err
})
func sendDigestErrorMail(e error, c appengine.Context, gitHubUserId int) {
func sendDigestErrorMail(e error, c context.Context, gitHubUserId int) {
if strings.Contains(e.Error(), ": 502") {
// Ignore 502s from GitHub, there's nothing we do about them.
return;
return
}
errorMessage := &mail.Message{
Sender: "RetroGit Admin <digests@retrogit.com>",
@ -280,11 +284,11 @@ func sendDigestErrorMail(e error, c appengine.Context, gitHubUserId int) {
}
err := mail.Send(c, errorMessage)
if err != nil {
c.Errorf("Error %s sending error email.", err.Error())
log.Errorf(c, "Error %s sending error email.", err.Error())
}
}
func sendDigestForAccount(account *Account, c appengine.Context) (bool, error) {
func sendDigestForAccount(account *Account, c context.Context) (bool, error) {
oauthTransport := githubOAuthTransport(c)
oauthTransport.Token = &account.OAuthToken
githubClient := github.NewClient(oauthTransport.Client())
@ -295,7 +299,7 @@ func sendDigestForAccount(account *Account, c appengine.Context) (bool, error) {
gitHubStatus := gitHubError.Response.StatusCode
if gitHubStatus == http.StatusUnauthorized ||
gitHubStatus == http.StatusForbidden {
c.Errorf(" GitHub auth error while getting email adddress, skipping: %s", err.Error())
log.Errorf(c, " GitHub auth error while getting email adddress, skipping: %s", err.Error())
return false, nil
}
}
@ -312,7 +316,7 @@ func sendDigestForAccount(account *Account, c appengine.Context) (bool, error) {
gitHubStatus := gitHubError.Response.StatusCode
if gitHubStatus == http.StatusUnauthorized ||
gitHubStatus == http.StatusForbidden {
c.Errorf(" GitHub auth error while getting digest, sending error email: %s", err.Error())
log.Errorf(c, " GitHub auth error while getting digest, sending error email: %s", err.Error())
var authErrorHtml bytes.Buffer
if err := templates["github-auth-error-email"].Execute(&authErrorHtml, nil); err != nil {
return false, err
@ -516,11 +520,11 @@ func setInitialTimezoneHandler(w http.ResponseWriter, r *http.Request, state *Ap
var cacheDigestForAccountFunc = delay.Func(
"cacheDigestForAccount",
func(c appengine.Context, githubUserId int) error {
c.Infof("Caching digest for %d...", githubUserId)
func(c context.Context, githubUserId int) error {
log.Infof(c, "Caching digest for %d...", githubUserId)
account, err := getAccount(c, githubUserId)
if err != nil {
c.Errorf(" Error looking up account: %s", err.Error())
log.Errorf(c, " Error looking up account: %s", err.Error())
// Not returning error since we don't want these tasks to be
// retried.
return nil
@ -531,9 +535,9 @@ var cacheDigestForAccountFunc = delay.Func(
githubClient := github.NewClient(oauthTransport.Client())
_, err = newDigest(c, githubClient, account)
if err != nil {
c.Errorf(" Error computing digest: %s", err.Error())
log.Errorf(c, " Error computing digest: %s", err.Error())
}
c.Infof(" Done!")
log.Infof(c, " Done!")
return nil
})
@ -544,12 +548,12 @@ func deleteAccountHandler(w http.ResponseWriter, r *http.Request, state *AppSign
return RedirectToRoute("index")
}
func githubOAuthTransport(c appengine.Context) *oauth.Transport {
appengineTransport := &urlfetch.Transport{Context: c}
appengineTransport.Deadline = time.Second * 60
func githubOAuthTransport(c context.Context) *oauth.Transport {
ctx_with_timeout, _ := context.WithTimeout(c, time.Second*60)
appengineTransport := &urlfetch.Transport{Context: ctx_with_timeout}
cachingTransport := &CachingTransport{
Transport: appengineTransport,
Context: c,
Context: ctx_with_timeout,
}
return &oauth.Transport{
Config: &githubOauthConfig,

View file

@ -1,4 +1,4 @@
package retrogit
package main
import (
"encoding/base64"
@ -6,7 +6,7 @@ import (
"io/ioutil"
"log"
"appengine"
"google.golang.org/appengine"
"github.com/gorilla/sessions"
)

View file

@ -1,4 +1,4 @@
package retrogit
package main
import (
"encoding/json"

9
deploy.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
# With the Go 1.11 runtime, if we're not using modules, all source (including
# the app itself) must live under GOPATH. Copy it there before deploying.
DEST="$GOPATH/src/retrogit"
rm -rf $DEST
cp -r app $DEST
cd $DEST
gcloud app deploy --project retro-git app.yaml