From 16fc128e0050585377f7eb8e45e986ff6f426160 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Tue, 30 Jun 2026 09:51:01 -0300 Subject: [PATCH 01/25] add myname var --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c2bec0368b..0b30523653 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,5 @@ go build -o notely && ./notely *This starts the server in non-database mode.* It will serve a simple webpage at `http://localhost:8080`. You do *not* need to set up a database or any interactivity on the webpage yet. Instructions for that will come later in the course! + +Gasta's version of Boot.dev's Notely app \ No newline at end of file From 6ef514825fec604491fce8b87b08453130724deb Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Tue, 30 Jun 2026 22:19:43 -0300 Subject: [PATCH 02/25] test basic ci config --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..664032071d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: ci + +on: + pull_request: + branches: [main] + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.26.0" + + - name: Force Failure + run: (exit 1) \ No newline at end of file From 3eb2f6b7bf9ef2469c318c0b089dedf5ab730bb4 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Tue, 30 Jun 2026 22:22:48 -0300 Subject: [PATCH 03/25] test go version on ci --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 664032071d..d64b828150 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,5 +18,5 @@ jobs: with: go-version: "1.26.0" - - name: Force Failure - run: (exit 1) \ No newline at end of file + - name: Check Go version + run: go version \ No newline at end of file From dc2eefbacc8dfee6ad42844c1eed300f26f9fd68 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 1 Jul 2026 22:47:43 -0300 Subject: [PATCH 04/25] add sample test --- internal/auth/get_api_key_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 internal/auth/get_api_key_test.go diff --git a/internal/auth/get_api_key_test.go b/internal/auth/get_api_key_test.go new file mode 100644 index 0000000000..0cb05da217 --- /dev/null +++ b/internal/auth/get_api_key_test.go @@ -0,0 +1,20 @@ +package auth + +import ( + "net/http" + "testing" +) + +func TestGetAPIKey(t *testing.T) { + header := http.Header{} + header.Set("Authorization", "ApiKey test") + + apiKey, err := GetAPIKey(header) + if err != nil { + t.Fatalf("Failed to get API key: %v", err) + } + if apiKey != "test" { + t.Fatalf("Expected API key to be 'test', got '%s'", apiKey) + } + +} From 4d075782657c912455c3942a9ce0d159e9358cce Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 1 Jul 2026 22:49:30 -0300 Subject: [PATCH 05/25] fail test ci --- .github/workflows/ci.yml | 4 ++-- internal/auth/get_api_key_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d64b828150..c6ac79c0a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,5 +18,5 @@ jobs: with: go-version: "1.26.0" - - name: Check Go version - run: go version \ No newline at end of file + - name: Run tests + run: go test ./... \ No newline at end of file diff --git a/internal/auth/get_api_key_test.go b/internal/auth/get_api_key_test.go index 0cb05da217..5ab50a86a7 100644 --- a/internal/auth/get_api_key_test.go +++ b/internal/auth/get_api_key_test.go @@ -7,7 +7,7 @@ import ( func TestGetAPIKey(t *testing.T) { header := http.Header{} - header.Set("Authorization", "ApiKey test") + header.Set("Authorization", "failed test") apiKey, err := GetAPIKey(header) if err != nil { From 5404f513c21d79b5a775cf95ddbeb0eb901a4b4d Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 1 Jul 2026 22:50:57 -0300 Subject: [PATCH 06/25] fix ci test --- internal/auth/get_api_key_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/get_api_key_test.go b/internal/auth/get_api_key_test.go index 5ab50a86a7..9d7d089c0b 100644 --- a/internal/auth/get_api_key_test.go +++ b/internal/auth/get_api_key_test.go @@ -7,7 +7,7 @@ import ( func TestGetAPIKey(t *testing.T) { header := http.Header{} - header.Set("Authorization", "failed test") + header.Set("Authorization", "ApiKey test1234567890") apiKey, err := GetAPIKey(header) if err != nil { From da68a3e28b8c9653f595410467e23f261406f6cc Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 1 Jul 2026 22:52:08 -0300 Subject: [PATCH 07/25] fix ci test --- internal/auth/get_api_key_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/get_api_key_test.go b/internal/auth/get_api_key_test.go index 9d7d089c0b..0cb05da217 100644 --- a/internal/auth/get_api_key_test.go +++ b/internal/auth/get_api_key_test.go @@ -7,7 +7,7 @@ import ( func TestGetAPIKey(t *testing.T) { header := http.Header{} - header.Set("Authorization", "ApiKey test1234567890") + header.Set("Authorization", "ApiKey test") apiKey, err := GetAPIKey(header) if err != nil { From a9c256ba0779ebdf7e8506c6ddecba3e93b72c16 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 1 Jul 2026 22:55:30 -0300 Subject: [PATCH 08/25] add cover tag --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6ac79c0a4..3f18ba412d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,4 +19,4 @@ jobs: go-version: "1.26.0" - name: Run tests - run: go test ./... \ No newline at end of file + run: go test ./... -cover \ No newline at end of file From 5b48e90d9b4fd572251ebc6f0430b3cd136b35e3 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Thu, 2 Jul 2026 17:42:27 -0300 Subject: [PATCH 09/25] add badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0b30523653..b54724f401 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![CI status](https://github.com/gcoria/learn-cicd-starter/actions/workflows/ci.yml/badge.svg) + # learn-cicd-starter (Notely) This repo contains the starter code for the "Notely" application for the "Learn CICD" course on [Boot.dev](https://boot.dev). From a59ecd9bc226e37e3ee58e406b478469ecb5ded5 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Thu, 2 Jul 2026 17:43:58 -0300 Subject: [PATCH 10/25] add badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b54724f401..ff6cd3f7dc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +https://github.com/gcoria/learn-cicd-starter/actions/workflows/ci.yml/badge.svg + ![CI status](https://github.com/gcoria/learn-cicd-starter/actions/workflows/ci.yml/badge.svg) # learn-cicd-starter (Notely) From 1e5a5191ac199d2d52d8b8d9f8b6f22ccbe300c8 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Thu, 2 Jul 2026 17:45:33 -0300 Subject: [PATCH 11/25] add badge --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ff6cd3f7dc..b54724f401 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -https://github.com/gcoria/learn-cicd-starter/actions/workflows/ci.yml/badge.svg - ![CI status](https://github.com/gcoria/learn-cicd-starter/actions/workflows/ci.yml/badge.svg) # learn-cicd-starter (Notely) From c78f18d4b6e6928f124ef106523f82973b2a2786 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Thu, 2 Jul 2026 17:49:35 -0300 Subject: [PATCH 12/25] go fmt test --- internal/auth/auth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f969aacf63..51c6208114 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -8,7 +8,8 @@ import ( var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") -// GetAPIKey - +// GetAPIKey + func GetAPIKey(headers http.Header) (string, error) { authHeader := headers.Get("Authorization") if authHeader == "" { From 6cbb574ec90e5b5940d61b924f0f02c2538454c8 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Fri, 3 Jul 2026 08:55:59 -0300 Subject: [PATCH 13/25] add style check in parallel in ci --- .github/workflows/ci.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f18ba412d..eddbbf9d6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,4 +19,20 @@ jobs: go-version: "1.26.0" - name: Run tests - run: go test ./... -cover \ No newline at end of file + run: go test ./... -cover + + style: + name: Style + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25.1" + + - name: Check formatting + run: test -z $(go fmt ./...) \ No newline at end of file From bf1a1dc479e1f9fab42b95cdb9b81009053ead9a Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Fri, 3 Jul 2026 09:04:35 -0300 Subject: [PATCH 14/25] add static check --- .github/workflows/ci.yml | 8 +++++++- main.go | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eddbbf9d6c..7b2d3766fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,4 +35,10 @@ jobs: go-version: "1.25.1" - name: Check formatting - run: test -z $(go fmt ./...) \ No newline at end of file + run: test -z $(go fmt ./...) + + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@latest + + - name: Run staticcheck + run: staticcheck ./... \ No newline at end of file diff --git a/main.go b/main.go index 19d7366c5f..a621713c2b 100644 --- a/main.go +++ b/main.go @@ -96,3 +96,8 @@ func main() { log.Printf("Serving on port: %s\n", port) log.Fatal(srv.ListenAndServe()) } + +func unused() { + // this function does nothing + // and is called nowhere +} From b97ff561e540fd5ca0ff662a51325046ce1c80c1 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Fri, 3 Jul 2026 09:06:12 -0300 Subject: [PATCH 15/25] fix statick check test --- main.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/main.go b/main.go index a621713c2b..19d7366c5f 100644 --- a/main.go +++ b/main.go @@ -96,8 +96,3 @@ func main() { log.Printf("Serving on port: %s\n", port) log.Fatal(srv.ListenAndServe()) } - -func unused() { - // this function does nothing - // and is called nowhere -} From 9526c4d45588c20c7045362fb1a2e47d774648e5 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Mon, 6 Jul 2026 23:12:05 -0300 Subject: [PATCH 16/25] gosec step --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b2d3766fb..48f86dc71b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,4 +41,7 @@ jobs: run: go install honnef.co/go/tools/cmd/staticcheck@latest - name: Run staticcheck - run: staticcheck ./... \ No newline at end of file + run: staticcheck ./... + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest \ No newline at end of file From 4ca208336fbff644d822785fef77b074cd8e5135 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Mon, 6 Jul 2026 23:13:45 -0300 Subject: [PATCH 17/25] move gosec step --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48f86dc71b..1a6db46ffb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: - name: Run tests run: go test ./... -cover + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest style: name: Style @@ -42,6 +45,3 @@ jobs: - name: Run staticcheck run: staticcheck ./... - - - name: Install gosec - run: go install github.com/securego/gosec/v2/cmd/gosec@latest \ No newline at end of file From 2f0cc46c8d238578fdbb4099c08bf42789b3d8ee Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Mon, 6 Jul 2026 23:14:25 -0300 Subject: [PATCH 18/25] move gosec step --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a6db46ffb..6ca6f50a36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,9 @@ jobs: - name: Install gosec run: go install github.com/securego/gosec/v2/cmd/gosec@latest + - name: Run gosec + run: gosec ./... + style: name: Style runs-on: ubuntu-latest From a815de36dcbf56fd3f7fe052f37b58fb0f06e021 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Mon, 6 Jul 2026 23:21:25 -0300 Subject: [PATCH 19/25] fix gosec security --- json.go | 5 ++++- main.go | 28 +++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/json.go b/json.go index 1e6e7985e1..5bcb7998ba 100644 --- a/json.go +++ b/json.go @@ -30,5 +30,8 @@ func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { return } w.WriteHeader(code) - w.Write(dat) + _, err = w.Write(dat) + if err != nil { + log.Printf("Error writing JSON: %s", err) + } } diff --git a/main.go b/main.go index 19d7366c5f..cfe060fbb0 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,13 @@ package main import ( "database/sql" "embed" + "errors" "io" "log" "net/http" "os" + "strconv" + "time" "github.com/go-chi/chi" "github.com/go-chi/cors" @@ -27,12 +30,23 @@ var staticFiles embed.FS func main() { err := godotenv.Load(".env") if err != nil { - log.Printf("warning: assuming default configuration. .env unreadable: %v", err) + if errors.Is(err, os.ErrNotExist) { + log.Println("warning: .env file not found, using environment defaults") + } else { + log.Printf("warning: .env unreadable, using environment defaults: %v", err) + } } port := os.Getenv("PORT") if port == "" { - log.Fatal("PORT environment variable is not set") + port = "8080" + } + portNum, err := strconv.Atoi(port) + if err != nil { + log.Fatalf("invalid PORT: %v", err) + } + if portNum < 1 || portNum > 65535 { + log.Fatalf("invalid PORT: port must be between 1 and 65535, got %d", portNum) } apiCfg := apiConfig{} @@ -89,10 +103,14 @@ func main() { router.Mount("/v1", v1Router) srv := &http.Server{ - Addr: ":" + port, - Handler: router, + Addr: ":" + strconv.Itoa(portNum), + Handler: router, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, } - log.Printf("Serving on port: %s\n", port) + log.Printf("Serving on port: %d", portNum) log.Fatal(srv.ListenAndServe()) } From 1b99fada98dd93a5a4a2bba00604d355b23dda14 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 8 Jul 2026 09:15:34 -0300 Subject: [PATCH 20/25] Add CD workflow to build on push to main. Co-authored-by: Cursor --- .github/workflows/cd.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000000..ee9e0a34d7 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,22 @@ +name: cd + +on: + push: + branches: [main] + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.26.0" + + - name: Build production binary + run: ./scripts/buildprod.sh From 6ac0dc218b993aa3ca89b46cc411614c4cbccded Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Fri, 17 Jul 2026 16:33:19 -0300 Subject: [PATCH 21/25] add internal doc --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2092f54e78..e2c10e488c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ out .env learn-cicd-starter notely +CLOUD_BUILD_SETUP.md From 780a1f5b8f4c1324bd164046f84ed3d1f04b24a6 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Mon, 3 Aug 2026 18:02:30 -0300 Subject: [PATCH 22/25] add gcp deploy --- .github/workflows/cd.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ee9e0a34d7..dc50e92f65 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -20,3 +20,16 @@ jobs: - name: Build production binary run: ./scripts/buildprod.sh + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_CREDENTIALS }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: notely-501812 + + - name: Build and push Docker image + run: gcloud builds submit --tag us-central1-docker.pkg.dev/notely-501812/notely-ar-repo/notely:latest . From 375a5a2b4404772b36af8527a761077c023fbc6d Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 5 Aug 2026 10:35:10 -0300 Subject: [PATCH 23/25] Deploy Notely to Cloud Run and update homepage heading. Co-authored-by: Cursor --- .github/workflows/cd.yml | 3 +++ static/index.html | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index dc50e92f65..d2aa39faf4 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -33,3 +33,6 @@ jobs: - name: Build and push Docker image run: gcloud builds submit --tag us-central1-docker.pkg.dev/notely-501812/notely-ar-repo/notely:latest . + + - name: Deploy to Cloud Run + run: gcloud run deploy notely --image us-central1-docker.pkg.dev/notely-501812/notely-ar-repo/notely:latest --region us-central1 --allow-unauthenticated --project notely-501812 --max-instances=4 diff --git a/static/index.html b/static/index.html index 72be101028..5d4ad73c09 100644 --- a/static/index.html +++ b/static/index.html @@ -7,7 +7,7 @@ -

Notely

+

Welcome to Notely

From 4739982b4b186176ca9fe415ce2a2a08c84e7ef6 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Wed, 5 Aug 2026 18:01:43 -0300 Subject: [PATCH 24/25] Run goose migrations in CD before Cloud Run deploy. Co-authored-by: Cursor --- .github/workflows/cd.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d2aa39faf4..5bf5d4639d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -9,6 +9,9 @@ jobs: name: Deploy runs-on: ubuntu-latest + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + steps: - name: Check out code uses: actions/checkout@v6 @@ -18,6 +21,9 @@ jobs: with: go-version: "1.26.0" + - name: Install goose + run: go install github.com/pressly/goose/v3/cmd/goose@latest + - name: Build production binary run: ./scripts/buildprod.sh @@ -34,5 +40,8 @@ jobs: - name: Build and push Docker image run: gcloud builds submit --tag us-central1-docker.pkg.dev/notely-501812/notely-ar-repo/notely:latest . + - name: Run database migrations + run: ./scripts/migrateup.sh + - name: Deploy to Cloud Run run: gcloud run deploy notely --image us-central1-docker.pkg.dev/notely-501812/notely-ar-repo/notely:latest --region us-central1 --allow-unauthenticated --project notely-501812 --max-instances=4 From fa1f52c43695c694928119484942948aba9855a8 Mon Sep 17 00:00:00 2001 From: Gaston Coria Date: Fri, 7 Aug 2026 09:47:56 -0300 Subject: [PATCH 25/25] add some learnings docs --- LEARNINGS.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 LEARNINGS.md diff --git a/LEARNINGS.md b/LEARNINGS.md new file mode 100644 index 0000000000..706e397969 --- /dev/null +++ b/LEARNINGS.md @@ -0,0 +1,56 @@ +# Learnings — Learn CI/CD (Notely) + +Notes from Boot.dev’s Learn CI/CD course, applied to this Notely Go app. + +## Continuous Integration (CI) + +- **Trigger on PRs, not only on merge.** The `ci` workflow runs on `pull_request` to `main`, so broken tests or style issues are caught before merge. +- **Separate concerns into parallel jobs.** `tests` and `style` run as independent jobs on `ubuntu-latest`. Failures are clearer, and the pipeline finishes faster when they run in parallel. +- **Pin the toolchain.** `actions/setup-go` with an explicit `go-version` keeps local and CI builds aligned. +- **Tests belong in CI.** `go test ./... -cover` runs on every PR. A failing test (intentionally or not) blocks the pipeline — that feedback loop is the point. +- **Style is automated, not optional.** + - `go fmt`: `test -z $(go fmt ./...)` fails if any file would be reformatted. + - `staticcheck`: catches bugs and smell that the compiler misses. +- **Security scanning in CI.** `gosec ./...` looks for common Go security issues. Treat findings as build failures, then fix them (don’t just silence the tool). +- **Status badges.** A badge in the README (e.g. for `ci.yml`) makes pipeline health visible without opening the Actions tab. + +## Continuous Deployment (CD) + +- **Deploy on push to `main`.** The `cd` workflow assumes `main` is the release branch: merge → build → ship. +- **Build for the target platform.** `scripts/buildprod.sh` cross-compiles with `CGO_ENABLED=0 GOOS=linux GOARCH=amd64` so the binary runs in a Linux container even if you develop on macOS. +- **Keep the image thin.** The Dockerfile is a slim Debian image that only adds the prebuilt `notely` binary and CA certs — no Go toolchain in the runtime image. +- **Migrations before (or with) deploy.** Run Goose (`./scripts/migrateup.sh`) in CD so the schema is ready before the new Cloud Run revision serves traffic. +- **Cap scale early.** `--max-instances=4` on Cloud Run limits surprise cost while learning. + +## Secrets & config + +- **Never commit secrets.** `.env` is gitignored. CI/CD reads `DATABASE_URL` and `GCP_CREDENTIALS` from GitHub Actions secrets. +- **Same app, different envs.** Locally, missing `DATABASE_URL` means “no DB mode.” In CD, the secret must be set or migrations and CRUD fail. +- **Service account JSON in Actions.** `google-github-actions/auth` with `credentials_json` from a secret authenticates `gcloud` without interactive login. + +## Google Cloud pieces + +- **Artifact Registry** stores the Docker image (`…/notely-ar-repo/notely:latest`). +- **Cloud Build** builds and pushes from the repo (`gcloud builds submit --tag …`). +- **Cloud Run** runs the container (`gcloud run deploy … --allow-unauthenticated` for a public demo). +- **IAM matters.** The default Compute Engine service account needs roles like Cloud Build builder and Storage object viewer on the Cloud Build bucket — otherwise you get errors such as `Permission 'storage.objects.get' denied`. Fixing IAM is part of making CD work, not optional ops trivia. See `CLOUD_BUILD_SETUP.md` for the exact bindings used here. + +## Workflow habits that stuck + +1. Open a PR → watch CI (tests, fmt, staticcheck, gosec). +2. Merge to `main` → CD builds Linux binary → Cloud Build image → migrate DB → deploy Cloud Run. +3. Prefer small, reversible steps: add a CI step, confirm it fails correctly, then make it pass. +4. Document one-off GCP/IAM fixes so the next deploy doesn’t start from “why is permission denied?” + +## Stack at a glance + +| Layer | Choice | +| --- | --- | +| App | Go + Chi, Turso/libSQL, Goose migrations | +| CI | GitHub Actions (`ci.yml`) | +| CD | GitHub Actions (`cd.yml`) → GCP | +| Runtime | Cloud Run + Artifact Registry | + +## Takeaway + +CI is the automated gate on every change; CD is the automated path from a green `main` to a running service. Most of the friction was not YAML syntax — it was making tests honest, keeping secrets out of git, and giving GCP service accounts the right permissions end to end.