From d256e99b55ceca73523fcfd71cb570cb6ec3e0cd Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 14:26:52 +0300 Subject: [PATCH 01/14] ci(lab3): add PR-gated workflow Signed-off-by: Tivdzualubem --- .github/workflows/ci.yml | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 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 000000000..0b1b90880 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: ci + +on: + push: + branches: [main] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + pull_request: + branches: [main] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + +permissions: + contents: read + +jobs: + vet: + name: vet-go-${{ matrix.go }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go: ['1.23', '1.24'] + defaults: + run: + working-directory: app + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ matrix.go }} + cache: true + cache-dependency-path: app/go.sum + - run: go vet ./... + + test: + name: test-go-${{ matrix.go }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go: ['1.23', '1.24'] + defaults: + run: + working-directory: app + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ matrix.go }} + cache: true + cache-dependency-path: app/go.sum + - run: go test -race -count=1 ./... + + lint: + name: lint + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: '1.24' + cache: true + cache-dependency-path: app/go.sum + - uses: golangci/golangci-lint-action@8564da7cb3c6866ed1da648ca8f00a258ef0c802 # v6.5.2 + with: + version: v2.5.0 + working-directory: app + args: --timeout=5m From 0572abb9483f2eefa9fd1ff497f2d1ce65faf6a5 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 14:47:50 +0300 Subject: [PATCH 02/14] ci(lab3): update golangci-lint action to v7 Signed-off-by: Tivdzualubem --- .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 0b1b90880..ad2e425bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: go-version: '1.24' cache: true cache-dependency-path: app/go.sum - - uses: golangci/golangci-lint-action@8564da7cb3c6866ed1da648ca8f00a258ef0c802 # v6.5.2 + - uses: golangci/golangci-lint-action@7119f3d5ddced62a10a044847a6c6bb0f7a5e76a # v7.0.0 with: version: v2.5.0 working-directory: app From 8963d9b9c4bf53ed3ab58ce5f2d495b759ae4d60 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 15:15:27 +0300 Subject: [PATCH 03/14] docs(lab3): add CI submission draft --- submissions/lab3.md | 180 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 submissions/lab3.md diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..b5518f910 --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,180 @@ +# Lab 3 Submission — CI/CD: A PR-Gated Pipeline for QuickNotes + +## Chosen Path + +I chose the GitHub Actions path because the course repository and my fork are hosted on GitHub, and the previous labs were already submitted through GitHub pull requests. + +## Task 1 — PR Gate + +### CI workflow + +CI configuration file: + + .github/workflows/ci.yml + +The workflow is named `ci` and runs on pull requests targeting `main` and pushes to `main`. + +The workflow uses path filters so CI runs only when these paths change: + + app/** + .github/workflows/ci.yml + +### Jobs configured + +The workflow defines three independent jobs: + +- `vet` +- `test` +- `lint` + +The `vet` job runs: + + go vet ./... + +The `test` job runs: + + go test -race -count=1 ./... + +The `lint` job runs `golangci-lint` with version: + + v2.5.0 + +### Runtime and security settings + +The workflow uses the pinned runner: + + ubuntu-24.04 + +The workflow uses least-privilege permissions: + + permissions: + contents: read + +Pinned actions used: + + actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + golangci/golangci-lint-action@7119f3d5ddced62a10a044847a6c6bb0f7a5e76a # v7.0.0 + +### PR links + +Fork PR with working GitHub Actions checks: + + https://github.com/tivdzualubem/DevOps-Intro/pull/3 + +Course PR: + + https://github.com/inno-devops-labs/DevOps-Intro/pull/967 + +### Green CI run evidence + +The fork PR showed all five checks passing: + +- `ci / lint` +- `ci / test-go-1.23` +- `ci / test-go-1.24` +- `ci / vet-go-1.23` +- `ci / vet-go-1.24` + +The successful checks confirm that the workflow runs the required independent units of work and that the PR gate is functional on my fork. + +### Local validation + +YAML validation command: + + python3 - <<'PY' + import yaml + with open(".github/workflows/ci.yml", "r", encoding="utf-8") as f: + yaml.safe_load(f) + print("YAML OK") + PY + +Output: + + YAML OK + +Local Go checks: + + cd app + go vet ./... + go test -race -count=1 ./... + +Output: + + ok quicknotes 1.031s + +### Design questions + +#### a) Why pin the runner version (`ubuntu-24.04`) instead of `ubuntu-latest`? + +Pinning `ubuntu-24.04` makes the CI environment predictable. `ubuntu-latest` can move to a newer runner image when GitHub updates its hosted environment. That can change installed packages, system libraries, compiler behavior, or default tooling. A workflow that passed yesterday could fail today without any change in the repository. Pinning the runner reduces this moving-target risk. + +#### b) Why split vet + test + lint into separate units? + +Splitting `vet`, `test`, and `lint` makes failures easier to diagnose because each check reports independently. It also allows the jobs to run in parallel, which reduces wall-clock feedback time. If everything were combined into one job, the first failure could stop the later checks from running, hiding other problems and making the feedback less useful. + +#### c) What real attack does SHA pinning prevent? + +SHA pinning prevents a workflow from silently executing changed or malicious action code when a tag is moved. Lecture 3 discussed the March 2025 `tj-actions/changed-files` compromise, where attackers rewrote tags to malicious versions that leaked secrets from public CI runs. Pinning actions to full commit SHAs makes the referenced action code immutable, so a moved tag cannot change what the workflow executes. + +#### d) What is `permissions:` and what principle is behind it? + +`permissions:` controls what access the automatically provided `GITHUB_TOKEN` has during a workflow run. Setting `contents: read` gives the workflow only the access needed to read the repository contents. This follows the principle of least privilege: automation should receive only the minimum permissions needed to do its job, so the damage is limited if a workflow step or action is compromised. + +#### e) GitLab path question + +I used the GitHub Actions path, not GitLab CI. In GitLab, a stage is a pipeline phase such as `test`, `scan`, or `publish`, while a job is an individual unit of work inside a stage. Jobs in the same stage can run in parallel, while stages usually run in order. `dependencies:` controls which artifacts from previous jobs are downloaded by a later job; it does not define the overall execution order the way `stages:` does. + +## Task 2 — Make It Fast and Smart + +### Optimizations applied + +The workflow uses Go dependency caching through `actions/setup-go`: + + cache: true + cache-dependency-path: app/go.sum + +The workflow runs `vet` and `test` as a matrix across two Go versions: + + Go 1.23 + Go 1.24 + +The matrix uses: + + fail-fast: false + +The workflow also uses path filters so CI runs only when `app/**` or `.github/workflows/ci.yml` changes. + +### Timing table + +| Scenario | Wall-clock | +|----------|-----------:| +| Baseline (no cache, single Go version, no path filter) | To capture | +| With cache | To capture | +| With cache + matrix | To capture | + +### Design questions for Task 2 + +#### f) Why cache `go.sum`-keyed inputs and not build outputs? + +`go.sum` identifies the exact module dependencies used by the project. Caching dependency inputs is safe because those modules are versioned and reproducible. Build outputs are generated artifacts and may depend on operating system, compiler flags, architecture, or local state. Caching generated outputs as trusted inputs can create subtle correctness and security problems. + +#### g) What does `fail-fast: false` change in a matrix run, and when would `fail-fast: true` be useful? + +`fail-fast: false` allows all matrix jobs to finish even if one matrix cell fails. This is useful when testing multiple Go versions because it shows exactly which version fails and which still passes. `fail-fast: true` is useful when the matrix is expensive and one failure is enough to invalidate the whole run, such as a deployment pipeline where continuing would waste time or CI minutes. + +#### h) What is the risk of malicious PR cache poisoning? + +A malicious pull request could try to write dangerous or corrupted cache content that later trusted branches restore and use. This is a supply-chain risk because cache content can cross workflow boundaries if not controlled. GitHub mitigates this by restricting cache access patterns, especially around protected and default branches, but workflows should still avoid caching untrusted generated outputs. + +## Task 1.5 — Failure and Fix Evidence + +To be completed after intentionally breaking a test and restoring it. + +## Branch Protection Evidence + +To be completed after requiring CI checks on the fork `main` branch. + +## Bonus Task — Pipeline Performance Investigation + +To be completed after collecting CI timing data. From e7f828c6279ce556c4b537a4b59e9eae9d3a1192 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 15:19:38 +0300 Subject: [PATCH 04/14] test(lab3): demonstrate failing PR gate --- app/handlers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/handlers_test.go b/app/handlers_test.go index 9dff2e3e5..f74b8f006 100644 --- a/app/handlers_test.go +++ b/app/handlers_test.go @@ -60,7 +60,7 @@ func TestCreateNote_RoundTrip(t *testing.T) { "title": "first", "body": "hello", }) - if rec.Code != http.StatusCreated { + if rec.Code != http.StatusOK { t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String()) } var n Note From 4c73fffac126914350365aaaaf821bd9c3312a07 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 15:25:57 +0300 Subject: [PATCH 05/14] test(lab3): restore passing PR gate --- app/handlers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/handlers_test.go b/app/handlers_test.go index f74b8f006..9dff2e3e5 100644 --- a/app/handlers_test.go +++ b/app/handlers_test.go @@ -60,7 +60,7 @@ func TestCreateNote_RoundTrip(t *testing.T) { "title": "first", "body": "hello", }) - if rec.Code != http.StatusOK { + if rec.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String()) } var n Note From 2e9088b0c59efaecf24a1e684c81a82d49f8f3a3 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 15:29:48 +0300 Subject: [PATCH 06/14] docs(lab3): document PR gate failure and fix --- submissions/lab3.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/submissions/lab3.md b/submissions/lab3.md index b5518f910..fc5b713d5 100644 --- a/submissions/lab3.md +++ b/submissions/lab3.md @@ -169,7 +169,44 @@ A malicious pull request could try to write dangerous or corrupted cache content ## Task 1.5 — Failure and Fix Evidence -To be completed after intentionally breaking a test and restoring it. +I intentionally broke the test suite in commit: + + e7f828c test(lab3): demonstrate failing PR gate + +The failing change modified `app/handlers_test.go` so that `TestCreateNote_RoundTrip` failed even though the application returned HTTP 201. + +Local failure output: + + --- FAIL: TestCreateNote_RoundTrip (0.00s) + handlers_test.go:64: expected 201, got 201 + FAIL + FAIL quicknotes 0.020s + +On the fork pull request, GitHub Actions showed: + + Some checks were not successful + 2 failing, 3 successful checks + ci / test-go-1.23 failed + ci / test-go-1.24 failed + Merging is blocked + +This demonstrates that the PR gate blocks a broken change. + +I then restored the test in commit: + + 4c73fff test(lab3): restore passing PR gate + +After the restore commit, the fork pull request showed: + + All checks have passed + 5 successful checks + ci / lint passed + ci / test-go-1.23 passed + ci / test-go-1.24 passed + ci / vet-go-1.23 passed + ci / vet-go-1.24 passed + +This confirms that the PR gate recovers after the fix and allows only passing code. ## Branch Protection Evidence From 816fa8dc26b09f36b989b5da0669fb7246f64b8c Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 15:51:30 +0300 Subject: [PATCH 07/14] docs(lab3): document branch protection checks --- submissions/lab3.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/submissions/lab3.md b/submissions/lab3.md index fc5b713d5..abfd4d110 100644 --- a/submissions/lab3.md +++ b/submissions/lab3.md @@ -210,7 +210,26 @@ This confirms that the PR gate recovers after the fix and allows only passing co ## Branch Protection Evidence -To be completed after requiring CI checks on the fork `main` branch. +The fork repository has a branch protection rule for `main`. + +The rule requires: + + Pull request before merging + 1 approval + Status checks to pass before merging + Branches to be up to date before merging + Signed commits + Linear history + +The required status checks configured on the fork are: + + lint + test-go-1.23 + test-go-1.24 + vet-go-1.23 + vet-go-1.24 + +This means a PR cannot be merged into `main` unless the CI pipeline passes and the protected branch requirements are satisfied. ## Bonus Task — Pipeline Performance Investigation From 2f58f62d829a2ada9e99f40952153dd457e6f665 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 15:57:17 +0300 Subject: [PATCH 08/14] docs(lab3): add CI timing evidence --- submissions/lab3.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/submissions/lab3.md b/submissions/lab3.md index abfd4d110..e9ecd14c4 100644 --- a/submissions/lab3.md +++ b/submissions/lab3.md @@ -147,11 +147,23 @@ The workflow also uses path filters so CI runs only when `app/**` or `.github/wo ### Timing table -| Scenario | Wall-clock | -|----------|-----------:| -| Baseline (no cache, single Go version, no path filter) | To capture | -| With cache | To capture | -| With cache + matrix | To capture | +The latest optimized GitHub Actions run on the fork PR produced the following timings: + +| Check | Result | Time | +|---|---:|---:| +| lint | passed | 25s | +| test-go-1.23 | passed | 29s | +| test-go-1.24 | passed | 27s | +| vet-go-1.23 | passed | 22s | +| vet-go-1.24 | passed | 23s | + +Because the jobs run in parallel, the total wall-clock feedback time is approximately the slowest job time, about 29 seconds. + +| Scenario | Wall-clock observation | +|----------|----------------------:| +| Baseline idea: single Go version, no dependency cache, no path filter | Slower expected because every run must resolve dependencies and cannot skip irrelevant paths | +| With cache | Faster after dependency cache is warm | +| With cache + matrix + parallel jobs + path filters | About 29s on the measured PR run | ### Design questions for Task 2 From 702621628d017c5655ed036fa739a01fcb4d9e84 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 22:09:58 +0300 Subject: [PATCH 09/14] docs(lab3): add CI performance bonus analysis --- submissions/lab3.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/submissions/lab3.md b/submissions/lab3.md index e9ecd14c4..d0d2b40d8 100644 --- a/submissions/lab3.md +++ b/submissions/lab3.md @@ -245,4 +245,30 @@ This means a PR cannot be merged into `main` unless the CI pipeline passes and t ## Bonus Task — Pipeline Performance Investigation -To be completed after collecting CI timing data. +### Performance target + +The target was to keep PR feedback under 90 seconds. The measured optimized run completed in about 29 seconds wall-clock time, so the pipeline is comfortably within the target. + +### Optimizations used + +The pipeline uses more than three optimizations: + +1. Independent parallel jobs for `vet`, `test`, and `lint`. +2. Go dependency caching through `actions/setup-go`. +3. A Go version matrix for `vet` and `test`, covering Go 1.23 and Go 1.24. +4. `fail-fast: false`, so all matrix results are collected even if one version fails. +5. Path filters, so documentation-only or unrelated changes do not trigger application CI. +6. Full SHA pinning for third-party actions, reducing supply-chain risk and making CI behavior more reproducible. +7. Least-privilege `GITHUB_TOKEN` permissions using `contents: read`. + +### Bottleneck analysis + +The measured check times were: + + lint: 25s + test-go-1.23: 29s + test-go-1.24: 27s + vet-go-1.23: 22s + vet-go-1.24: 23s + +The slowest job was `test-go-1.23` at 29 seconds. Because the jobs run in parallel, this slowest job determines the overall feedback time. From ce274fcaa78ab5f10e554876bee1b47a3a7ae7a9 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 22:26:51 +0300 Subject: [PATCH 10/14] ci(lab3): measure baseline workflow --- .github/workflows/ci.yml | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad2e425bc..0759160b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,26 +3,16 @@ name: ci on: push: branches: [main] - paths: - - 'app/**' - - '.github/workflows/ci.yml' pull_request: branches: [main] - paths: - - 'app/**' - - '.github/workflows/ci.yml' permissions: contents: read jobs: vet: - name: vet-go-${{ matrix.go }} + name: vet-baseline-go-1.24 runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - go: ['1.23', '1.24'] defaults: run: working-directory: app @@ -30,18 +20,13 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: ${{ matrix.go }} - cache: true - cache-dependency-path: app/go.sum + go-version: '1.24' + cache: false - run: go vet ./... test: - name: test-go-${{ matrix.go }} + name: test-baseline-go-1.24 runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - go: ['1.23', '1.24'] defaults: run: working-directory: app @@ -49,21 +34,19 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: ${{ matrix.go }} - cache: true - cache-dependency-path: app/go.sum + go-version: '1.24' + cache: false - run: go test -race -count=1 ./... lint: - name: lint + name: lint-baseline runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.24' - cache: true - cache-dependency-path: app/go.sum + cache: false - uses: golangci/golangci-lint-action@7119f3d5ddced62a10a044847a6c6bb0f7a5e76a # v7.0.0 with: version: v2.5.0 From 15899a38c3415e0f9d39acb0ed0166b83396ec96 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 22:34:36 +0300 Subject: [PATCH 11/14] ci(lab3): measure cache-only workflow --- .github/workflows/ci.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0759160b3..44e6cd4c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ permissions: jobs: vet: - name: vet-baseline-go-1.24 + name: vet-cache-go-1.24 runs-on: ubuntu-24.04 defaults: run: @@ -21,11 +21,12 @@ jobs: - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.24' - cache: false + cache: true + cache-dependency-path: app/go.sum - run: go vet ./... test: - name: test-baseline-go-1.24 + name: test-cache-go-1.24 runs-on: ubuntu-24.04 defaults: run: @@ -35,18 +36,20 @@ jobs: - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.24' - cache: false + cache: true + cache-dependency-path: app/go.sum - run: go test -race -count=1 ./... lint: - name: lint-baseline + name: lint-cache runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.24' - cache: false + cache: true + cache-dependency-path: app/go.sum - uses: golangci/golangci-lint-action@7119f3d5ddced62a10a044847a6c6bb0f7a5e76a # v7.0.0 with: version: v2.5.0 From a7fecab36ad401caf8fbfb01bcc339e124124915 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 22:41:08 +0300 Subject: [PATCH 12/14] ci(lab3): restore optimized workflow --- .github/workflows/ci.yml | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44e6cd4c2..3d100d10c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,16 +3,33 @@ name: ci on: push: branches: [main] + paths: + - 'app/**' + - '.github/workflows/ci.yml' pull_request: branches: [main] + paths: + - 'app/**' + - '.github/workflows/ci.yml' permissions: contents: read +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + GOFLAGS: -buildvcs=false + jobs: vet: - name: vet-cache-go-1.24 + name: vet-go-${{ matrix.go }} runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go: ['1.23', '1.24'] defaults: run: working-directory: app @@ -20,14 +37,18 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: '1.24' + go-version: ${{ matrix.go }} cache: true cache-dependency-path: app/go.sum - run: go vet ./... test: - name: test-cache-go-1.24 + name: test-go-${{ matrix.go }} runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go: ['1.23', '1.24'] defaults: run: working-directory: app @@ -35,13 +56,13 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: '1.24' + go-version: ${{ matrix.go }} cache: true cache-dependency-path: app/go.sum - run: go test -race -count=1 ./... lint: - name: lint-cache + name: lint runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From cc8ffebe31ab9ecd701ba047efd928d0f9be13b9 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 22:44:48 +0300 Subject: [PATCH 13/14] docs(lab3): demonstrate docs-only CI skip --- submissions/lab3-docs-only-skip-check.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 submissions/lab3-docs-only-skip-check.md diff --git a/submissions/lab3-docs-only-skip-check.md b/submissions/lab3-docs-only-skip-check.md new file mode 100644 index 000000000..05ef4afd6 --- /dev/null +++ b/submissions/lab3-docs-only-skip-check.md @@ -0,0 +1,3 @@ +# Lab 3 Docs-only Skip Check + +This temporary documentation-only file is used to demonstrate that the CI path filter does not run application CI for changes outside app/** and .github/workflows/ci.yml. From b0b26cad4c4b6564e60d980d2a8d58d9e1562697 Mon Sep 17 00:00:00 2001 From: Tivdzualubem Date: Tue, 9 Jun 2026 22:50:03 +0300 Subject: [PATCH 14/14] docs(lab3): add measured timing and skip evidence --- submissions/lab3-docs-only-skip-check.md | 3 - submissions/lab3.md | 85 +++++++++++++++--------- 2 files changed, 53 insertions(+), 35 deletions(-) delete mode 100644 submissions/lab3-docs-only-skip-check.md diff --git a/submissions/lab3-docs-only-skip-check.md b/submissions/lab3-docs-only-skip-check.md deleted file mode 100644 index 05ef4afd6..000000000 --- a/submissions/lab3-docs-only-skip-check.md +++ /dev/null @@ -1,3 +0,0 @@ -# Lab 3 Docs-only Skip Check - -This temporary documentation-only file is used to demonstrate that the CI path filter does not run application CI for changes outside app/** and .github/workflows/ci.yml. diff --git a/submissions/lab3.md b/submissions/lab3.md index d0d2b40d8..6da4aeca6 100644 --- a/submissions/lab3.md +++ b/submissions/lab3.md @@ -147,23 +147,44 @@ The workflow also uses path filters so CI runs only when `app/**` or `.github/wo ### Timing table -The latest optimized GitHub Actions run on the fork PR produced the following timings: +I measured three workflow states from the GitHub PR checks UI. -| Check | Result | Time | -|---|---:|---:| -| lint | passed | 25s | -| test-go-1.23 | passed | 29s | -| test-go-1.24 | passed | 27s | -| vet-go-1.23 | passed | 22s | -| vet-go-1.24 | passed | 23s | +| Scenario | Configuration | Wall-clock | +|----------|---------------|-----------:| +| Baseline | Single Go 1.24, no dependency cache, no path filter | 27s | +| With cache | Single Go 1.24, dependency cache enabled, no matrix | 34s | +| With cache + matrix + path filters | Go 1.23/1.24 matrix, dependency cache, path filters, parallel jobs | 29s | -Because the jobs run in parallel, the total wall-clock feedback time is approximately the slowest job time, about 29 seconds. +Detailed measured checks: -| Scenario | Wall-clock observation | -|----------|----------------------:| -| Baseline idea: single Go version, no dependency cache, no path filter | Slower expected because every run must resolve dependencies and cannot skip irrelevant paths | -| With cache | Faster after dependency cache is warm | -| With cache + matrix + parallel jobs + path filters | About 29s on the measured PR run | +| Scenario | Check | Time | +|---|---|---:| +| Baseline | lint-baseline | 27s | +| Baseline | test-baseline-go-1.24 | 23s | +| Baseline | vet-baseline-go-1.24 | 23s | +| Cache-only | lint-cache | 23s | +| Cache-only | test-cache-go-1.24 | 34s | +| Cache-only | vet-cache-go-1.24 | 24s | +| Final optimized | lint | 29s | +| Final optimized | test-go-1.23 | 27s | +| Final optimized | test-go-1.24 | 28s | +| Final optimized | vet-go-1.23 | 22s | +| Final optimized | vet-go-1.24 | 24s | + +The cache-only run was slower than the baseline in this measurement because hosted runner scheduling and cache restore behavior vary between runs. The final optimized pipeline still meets the performance target because the matrix jobs run in parallel and the total feedback time is determined by the slowest job. + +### Docs-only skip demonstration + +I added a temporary documentation-only commit: + + cc8ffeb docs(lab3): demonstrate docs-only CI skip + +The changed file was outside both workflow trigger paths: + + app/** + .github/workflows/ci.yml + +The PR did not start a new application CI run for that docs-only change. The PR continued showing the previous required optimized checks as successful. This demonstrates that the path filter skips documentation-only changes. ### Design questions for Task 2 @@ -247,28 +268,28 @@ This means a PR cannot be merged into `main` unless the CI pipeline passes and t ### Performance target -The target was to keep PR feedback under 90 seconds. The measured optimized run completed in about 29 seconds wall-clock time, so the pipeline is comfortably within the target. +The target was to keep PR feedback under 90 seconds. The final optimized run completed in about 29 seconds wall-clock time, so the pipeline is comfortably within the target. -### Optimizations used +### Additional optimizations beyond Task 2 -The pipeline uses more than three optimizations: +The final workflow includes these extra optimizations and hardening choices beyond the basic cache + matrix + path filter requirements: -1. Independent parallel jobs for `vet`, `test`, and `lint`. -2. Go dependency caching through `actions/setup-go`. -3. A Go version matrix for `vet` and `test`, covering Go 1.23 and Go 1.24. -4. `fail-fast: false`, so all matrix results are collected even if one version fails. -5. Path filters, so documentation-only or unrelated changes do not trigger application CI. -6. Full SHA pinning for third-party actions, reducing supply-chain risk and making CI behavior more reproducible. -7. Least-privilege `GITHUB_TOKEN` permissions using `contents: read`. +1. Independent `vet`, `test`, and `lint` jobs run in parallel. +2. `GOFLAGS=-buildvcs=false` is set to avoid unnecessary VCS stamping work in CI. +3. `concurrency` cancels older duplicate runs when a newer commit is pushed to the same PR. +4. Full SHA pinning is used for actions, making the pipeline reproducible and reducing supply-chain risk. +5. Least-privilege `permissions: contents: read` limits the workflow token. -### Bottleneck analysis +### Before/after timing table + +| Optimization applied | Before | After | Saving | +|---|---:|---:|---:| +| Parallel independent jobs | Serial-style total would be about 73s using baseline check sum | 27s wall-clock baseline parallel run | about 46s | +| Add cache-only setup | 27s baseline wall-clock | 34s measured cache-only wall-clock | no saving in this run | +| Restore final matrix + path-filter workflow | 34s cache-only wall-clock | 29s optimized wall-clock | about 5s | +| Docs-only path filter | Full CI run would be about 29s | skipped app CI for docs-only commit | about 29s saved | -The measured check times were: +### Bottleneck analysis - lint: 25s - test-go-1.23: 29s - test-go-1.24: 27s - vet-go-1.23: 22s - vet-go-1.24: 23s +The dominant remaining cost is the test job, especially `go test -race -count=1 ./...`, because the race detector adds runtime compared with a normal test run. I kept the race detector because the lab explicitly requires it and because it is a useful quality gate for catching concurrency issues. To make QuickNotes itself faster, the application tests would need to reduce unnecessary setup work, avoid slow integration-style paths where unit tests are enough, and keep test data minimal. I would stop optimizing this pipeline below roughly 30 seconds because the remaining time is mostly runner startup and required quality checks, and further reduction would not justify weakening the gate. -The slowest job was `test-go-1.23` at 29 seconds. Because the jobs run in parallel, this slowest job determines the overall feedback time.