diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..1a68db5e5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (≤ 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d34048fb4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +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 (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + go-version: ["1.23.x", "1.24.x"] + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 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 + + - name: Run go vet + run: go vet ./... + + test: + name: test (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + go-version: ["1.23.x", "1.24.x"] + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 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 + + - name: Run tests + run: go test -race -count=1 ./... + + lint: + name: lint + runs-on: ubuntu-24.04 + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 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 + + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + + - name: Run golangci-lint + run: golangci-lint run diff --git a/screenshots/lab3/blockedmerge.png b/screenshots/lab3/blockedmerge.png new file mode 100644 index 000000000..8017107b9 Binary files /dev/null and b/screenshots/lab3/blockedmerge.png differ diff --git a/screenshots/lab3/failingci.png b/screenshots/lab3/failingci.png new file mode 100644 index 000000000..24c665f8a Binary files /dev/null and b/screenshots/lab3/failingci.png differ diff --git a/screenshots/lab3/greenci.png b/screenshots/lab3/greenci.png new file mode 100644 index 000000000..d66554777 Binary files /dev/null and b/screenshots/lab3/greenci.png differ diff --git a/screenshots/lab3/greenciafter.png b/screenshots/lab3/greenciafter.png new file mode 100644 index 000000000..a4f1bcad5 Binary files /dev/null and b/screenshots/lab3/greenciafter.png differ diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..4aaac79af --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,87 @@ +# Lab 3 Submission + +## Chosen path + +I chose GitHub Actions because my repository and PR workflow are already on GitHub. + +## Task 1 — PR gate + +### Green CI run + +Link: https://github.com/Mysteri0K1ng/DevOps-Intro/pull/3/checks + +![Green CI checks](../screenshots/lab3/greenci.png) + +### Failed CI run + +screenshot: + +![Failing CI checks](../screenshots/lab3/failingci.png) + +### Fix commit + +Commit: + +```bash +git revert HEAD -S -s +git push +``` + +![Green CI checks](../screenshots/lab3/greenciafter.png) + +### Branch protection + +Screenshot: + +![Blocked merge](../screenshots/lab3/blockedmerge.png) + +## Design questions + +### a) Why pin the runner version instead of `ubuntu-latest`? + +Pinning the runner version, for example ubuntu-24.04, makes the CI environment predictable and reproducible. If I use ubuntu-latest, GitHub can later move it to a newer image with different system packages, Go tooling, or defaults, and the pipeline may start failing even though the project code did not change. + +### b) Why split vet, test, and lint into separate units? + +I split vet, test, and lint into separate jobs so the PR shows exactly which kind of quality gate failed. If everything was in one combined job, the first failing command could stop the whole job and hide the results of the other checks. Separate jobs also run in parallel, which makes the pipeline easier to debug and usually faster in wall-clock time. + +### c) What real attack does SHA pinning prevent? + +SHA pinning helps prevent supply-chain attacks where an action tag, such as v1 or v4, is moved or compromised after I already trusted it. A real example is the March 2025 tj-actions/changed-files supply-chain incident, where using a mutable action reference could cause CI to execute compromised code. Pinning to a full commit SHA makes the workflow use one exact reviewed version instead of whatever code a tag points to later. + +### d) What is `permissions:` and what principle is behind it? + +permissions: controls what access the GitHub Actions GITHUB_TOKEN has during the workflow. Setting permissions: contents: read gives the pipeline only the access it needs to checkout and read the repository. This follows the principle of least privilege: CI jobs should not receive write access, secret access, or other powers unless they actually need them. + +## Task 2 — Make it fast and smart + +### Timing table + +| Scenario | Wall-clock | +|----------|------------| +| Baseline | 1m 13s | +| With cache | 1m 18s | +| With cache + matrix | 1m 13s | + +### Optimizations applied + +I enabled Go dependency caching using `app/go.sum` as the cache key. This lets CI reuse downloaded modules when dependencies do not change, although in my run the cache was slightly slower because of cache overhead and runner variance. + +I added a Go matrix for `1.23.x` and `1.24.x` for `vet` and `test`. This checks compatibility with two toolchains while still keeping wall-clock time low because the jobs run in parallel. + +I added path filters so CI runs only when `app/**` or the workflow file changes. This avoids wasting CI minutes on unrelated documentation-only changes. + +I kept `vet`, `test`, and `lint` as separate jobs so failures are easier to understand and independent checks can run in parallel. + +### f) Why cache `go.sum`-keyed inputs and not build outputs? + +Caching with a key based on go.sum is safer because go.sum describes the exact module dependencies that the Go project needs. These inputs are deterministic: if go.sum does not change, the downloaded modules should be the same. Build outputs are less reliable to cache because they can depend on the OS image, Go version, compiler flags, race detector settings, and other environment details, so reusing them can create subtle or stale results. + +### g) What does `fail-fast: false` change? + +fail-fast: false means that if one matrix job fails, GitHub Actions will not cancel the other matrix jobs. This is useful here because I want to see whether the failure happens only on Go 1.23, only on Go 1.24, or on both versions. I would use fail-fast: true when the jobs are expensive and one failure is already enough information, for example in a large deployment pipeline where continuing other matrix jobs would waste CI minutes. + +### h) What is the cache poisoning risk? + +The risk is cache poisoning: a malicious PR could try to store modified files or tools in the CI cache, and later a trusted branch could restore and execute that poisoned cache. This could turn an untrusted PR run into influence over a protected branch run. GitHub reduces this risk by scoping cache access between branches: workflow runs can restore caches from the current branch and default branch, but parent or sibling branches cannot read caches created by unrelated child branches. Even with those mitigations, I should avoid caching executable untrusted outputs and prefer dependency caches keyed by trusted files such as go.sum. +