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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Goal
<!-- What does this PR accomplish? 1 sentence. -->

## Changes
-

## Testing
<!-- How did you verify it? -->

## Checklist
- [ ] Title is a clear sentence (<= 70 chars)
- [ ] Commits are signed (`git log --show-signature`)
- [ ] `submissions/labN.md` updated
124 changes: 124 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: quicknotes-ci

on:
push:
branches:
- main
paths:
- app/**
- .github/workflows/ci.yml
pull_request:
branches:
- main
paths:
- app/**
- .github/workflows/ci.yml

permissions:
contents: read

env:
CGO_ENABLED: "1"
GOFLAGS: -buildvcs=false

defaults:
run:
shell: bash

jobs:
vet:
name: vet (go ${{ matrix.go-version }})
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
go-version:
- 1.23.x
- 1.24.x
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
cache: true
cache-dependency-path: |
app/go.sum
app/go.mod

- name: Run go vet
working-directory: app
run: go vet ./...

test:
name: test (go ${{ matrix.go-version }})
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
go-version:
- 1.23.x
- 1.24.x
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
cache: true
cache-dependency-path: |
app/go.sum
app/go.mod

- name: Run race tests
working-directory: app
run: go test -race -count=1 ./...

lint:
name: lint
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.24.x
cache: true
cache-dependency-path: |
app/go.sum
app/go.mod

- name: Run golangci-lint
uses: golangci/golangci-lint-action@25e2cdc5eb1d7a04fdc45ff538f1a00e960ae128 # v8.0.0
with:
version: v2.5.0
working-directory: app
args: --timeout=2m

ci-ok:
name: ci-ok
if: always()
needs:
- vet
- test
- lint
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check required jobs
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
exit 1
fi
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/lab3-baseline-with-cache.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/lab3-baseline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/lab3-branch-protection.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/lab3-docs-only.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/lab3-failed-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions submissions/lab3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Lab 3 Submission

Chosen path: GitHub Actions.

I chose GitHub Actions because the course repository and my fork are already on GitHub, so the PR gate can run in the same place where the review and branch protection rules live.

## Task 1 - PR Gate

### CI Configuration

Workflow file: `.github/workflows/ci.yml`

The workflow runs three independent units of work:

* `vet (go 1.23.x)` and `vet (go 1.24.x)` run `go vet ./...` in `app/`.

* `test (go 1.23.x)` and `test (go 1.24.x)` run `go test -race -count=1 ./...` in `app/`.

* `lint` runs `golangci-lint run` in `app/` with `golangci-lint` pinned to `v2.5.0`.

* `ci-ok` aggregates the required jobs so branch protection can require one stable check name even though the matrix expands `vet` and `test`.

The workflow uses:

* Pinned runner: `ubuntu-24.04`.

* Least-privilege token permissions: `contents: read`.

* Full-SHA action pins with readable version comments.

* `actions/setup-go` dependency caching.

* Path filters for `app/**` and `.github/workflows/ci.yml`.

### Evidence To Add After PR Runs

Green CI run:

```text
https://github.com/BearAx/DevOps-Intro/actions/runs/27604560723/job/81613358274?pr=4
```

Deliberate failing run from Task 1.5:

```text
Failed run: https://github.com/BearAx/DevOps-Intro/pull/4/checks?sha=4d40acc2249c4e61722a40516b4a8d505fe59174
fix commit SHA: 1cbfae257978ad42747b91057984d501ad6b6086
```

![Green CI run](../screenshots/lab3-failed-run.png)

Branch protection evidence:

![Branch protection evidence](../screenshots/lab3-branch-protection.png)


### Design Questions

#### a) Why pin `ubuntu-24.04` instead of `ubuntu-latest`?

`ubuntu-latest` is a moving pointer. GitHub can retarget it to a newer image, which may change preinstalled tools, shell behavior, OpenSSL versions, package versions, or default compiler behavior without a commit in this repo. Pinning `ubuntu-24.04` makes the CI environment predictable and keeps failures tied to our changes instead of a surprise runner image migration.

#### b) Why split vet, test, and lint into separate units?

Separate jobs make failures easier to diagnose because a vet failure, test failure, and lint failure appear as different checks. They also run in parallel, so the wall-clock time is the slowest job instead of the sum of all three commands. A single combined job is simpler but slower and less precise: the first failing command hides later failures until another run.

#### c) What real attack does SHA pinning prevent?

SHA pinning prevents tag-retargeting attacks against reusable actions. Lecture 3 cites the March 2025 `tj-actions/changed-files` compromise, where attackers rewrote action tags to a malicious version and leaked secrets from public CI runs. If a workflow pins a full commit SHA, moving `v1`, `v4`, or another tag cannot silently change what code runs in CI.

#### d) What is `permissions:` and what principle is behind it?

`permissions:` controls the default `GITHUB_TOKEN` privileges granted to the workflow. Setting `contents: read` follows least privilege: the workflow can read repository content, but it cannot write code, create releases, modify pull requests, or perform unrelated privileged actions unless explicitly granted.

#### e) GitLab path: stages, jobs, and dependencies

Not applicable to my chosen GitHub Actions path. In GitLab CI, a job is one unit of work, while a stage groups jobs and controls broad execution order. `dependencies:` is more specific than `stages:` because it controls which previous-job artifacts a job downloads; stage order alone only says when jobs may run.

## Task 2 - Make It Fast and Smart

### Optimizations Applied

Caching:

`actions/setup-go` has `cache: true` and uses `app/go.sum` plus `app/go.mod` as dependency inputs. QuickNotes currently has no external module dependencies, so `app/go.sum` may not exist; including `app/go.mod` still gives the cache a stable module-input key.

Matrix:

The `vet` and `test` jobs run against `1.23.x` and `1.24.x` with `fail-fast: false`, so both Go versions report their result even if one fails.
The `ci-ok` job uses `if: always()` and fails if any required job fails or is cancelled, which avoids branch-protection churn when matrix check names change.

Path filters:

The workflow runs only when `app/**` or `.github/workflows/ci.yml` changes. A docs-only PR that touches only root documentation should not spend CI minutes on QuickNotes checks.

Additional small optimization:

The workflow sets `GOFLAGS=-buildvcs=false`, which avoids VCS stamping work in CI clones where full Git metadata is not needed for these checks.

### Timing Table

```text
| Scenario | Wall-clock |
|------------------------------------------------------|-----------:|
| Baseline: no cache, single Go version, no path filter | 39s |
| With cache | 49s |
| With cache + matrix | 38s |
```
![Timing Table](../screenshots/lab3-baseline.png)
![Timing Table](../screenshots/lab3-baseline-with-cache.png)
![Timing Table](../screenshots/lab3-baseline-with-cache-and-matrix.png)

The cache did not significantly reduce total wall-clock time because QuickNotes has no third-party dependencies. Most time was spent on runner provisioning, checkout, setup-go, and linter setup rather than module download.

Docs-only skip evidence:

```text
URL of PR: https://github.com/BearAx/DevOps-Intro/pull/6
```
![Docs-only skip evidence](../screenshots/lab3-docs-only.png)

### Design Questions

#### f) Why cache `go.sum`-keyed inputs and not build outputs?

The dependency inputs are deterministic: `go.mod` and `go.sum` describe exactly which module versions should be downloaded. Caching those inputs lets CI reuse downloaded modules and build cache entries while still rebuilding the project from source. Treating final build outputs as reusable artifacts is riskier because outputs can depend on OS image details, compiler flags, environment variables, architecture, or poisoned state from a previous run.

#### g) What does `fail-fast: false` change in a matrix run?

With `fail-fast: false`, GitHub lets every matrix cell finish even after one cell fails. That is useful here because we want to know whether the failure is specific to Go `1.23.x`, Go `1.24.x`, or both. I would use `fail-fast: true` for expensive deploy or integration matrices where one failure proves the candidate is bad and saving runner minutes matters more than collecting all combinations.

#### h) What is the cache poisoning risk from malicious PRs?

If an untrusted PR could write a cache that protected branches later restore, the attacker could plant malicious tools, compiled objects, or dependency state and wait for trusted CI to execute or reuse it. GitHub mitigates this by scoping cache access by branch and event so pull requests can generally read caches from the base branch but cannot overwrite caches used by protected default-branch runs. The safe design is still to key caches from trusted dependency files and avoid executing arbitrary cached binaries as authority.

## Bonus Task - Pipeline Performance Investigation

Not attempted.