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
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
paths:
- 'app/**'
- '.github/workflows/**'

permissions:
contents: read

jobs:
vet:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go: ['1.23', '1.24']
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: ${{ matrix.go }}
cache-dependency-path: app/go.mod
- name: go vet
working-directory: app
run: go vet ./...

test:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go: ['1.23', '1.24']
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: ${{ matrix.go }}
cache-dependency-path: app/go.mod
- name: go test -race
working-directory: app
run: go test -race -count=1 ./...

lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: '1.24'
cache-dependency-path: app/go.mod
- uses: golangci/golangci-lint-action@9fae48acfc02a90574d7c304a1758ef9895495fa # v7.0.1
with:
version: v2.5.0
working-directory: app

ci-ok:
if: always()
needs: [vet, test, lint]
runs-on: ubuntu-24.04
steps:
- run: |
test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false"
Binary file added submissions/failed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions submissions/lab1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Lab 1 submission

## Task 1
Request:
```
curl -s http://localhost:8080/health | python3 -m json.tool
```

Answer:
```
{
"notes": 5,
"status": "ok"
}
```

Request:
```
curl -s http://localhost:8080/notes | python3 -m json.tool
```

Answer:
```
[
{
"id": 2,
"title": "Read app/main.go first",
"body": "Start by understanding the entry point \u2014 env vars, signal handling, graceful shutdown.",
"created_at": "2026-01-15T10:05:00Z"
},
{
"id": 3,
"title": "DevOps mantra",
"body": "If it hurts, do it more often.",
"created_at": "2026-01-15T10:10:00Z"
},
{
"id": 4,
"title": "Endpoint cheat-sheet",
"body": "GET /notes GET /notes/{id} POST /notes DELETE /notes/{id} GET /health GET /metrics",
"created_at": "2026-01-15T10:15:00Z"
},
{
"id": 1,
"title": "Welcome to QuickNotes",
"body": "This is the project you'll containerize, deploy, monitor, and harden across all 10 labs.",
"created_at": "2026-01-15T10:00:00Z"
}
]
```

Request:
```
curl -s -X POST http://localhost:8080/notes \
-H 'Content-Type: application/json' \
-d '{"title":"hello","body":"first POST"}' | python3 -m json.tool
```

Answer:
```
{
"id": 5,
"title": "hello",
"body": "first POST",
"created_at": "2026-06-05T10:51:13.503497Z"
},
```

```
git log --show-signature -1

commit 843a27f3ade36ea41d723f168fb3f8c9c1f7b70c (HEAD -> feature/lab1, origin/feature/lab1)
Good "git" signature for 15dnau@gmail.com with ED25519 key SHA256:k0n7/mx/uRX52s/zu9pxaN+h/IKnBJzcnuybJgthVkM
Author: Dmitrii <15dnau@gmail.com>
Date: Fri Jun 5 14:03:50 2026 +0300

docs(lab1): start submission

Signed-off-by: Dmitrii <15dnau@gmail.com>
```

### Verified commit

![Verified commit](verified.png "Verified commit")

When we work with Github we trust that commit made by Dmitrii was actually made by Dmitrii. However Git itself does not verify commit's author. Anyone can set any name and make a commit, therefore we want commits to be verified.

### GitHub Community
Why starring repositories matters in open source
For a project, stars are a signal of trust and relevance. Moreover, starring is something like bookmarking a repository.

How following developers helps in team projects and professional growth
Following your colleagues on GitHub gives you a low-noise feed of their activity
148 changes: 148 additions & 0 deletions submissions/lab3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
GitHub. I have some troubles with signing into Innopolis GitLab account.

### Green CI run
https://github.com/Dnau15/DevOps-Intro/actions/runs/27645595628


All three units passed: `vet`, `test`, `lint`.

### Failed run + fix (Task 1.5)
To prove the gate blocks a broken PR, I changed an expected value in
`app/handlers_test.go`, pushed, and the `test` check went **red** and the
PR became un-mergeable.

- ❌ Failed run: `<link to the red run>`
- 🔧 Fix commit (reverted the breakage, check green again): `<commit SHA / link>`

![failed run](failed.png)

### Branch protection (Task 1.6)
`main` on my fork requires the status checks to pass before merging, and
requires branches to be up to date.

![branch protection](protected_lab3.png)

---

## Design Questions (1.2)

### a) Why pin `ubuntu-24.04` instead of `ubuntu-latest`?
`ubuntu-latest` is a **moving alias**: GitHub periodically re-points it to a
newer LTS (e.g. 22.04 → 24.04). When that flip happens, the pre-installed
toolchain, system libraries (glibc), and default packages change **under
you** — a PR that was green yesterday can fail today with zero code changes,
and the failure is hard to diagnose because nothing in your repo moved.
Pinning `ubuntu-24.04` makes the environment **reproducible**: the same
commit builds in the same environment months later. What breaks otherwise:
silent toolchain/library bumps, removed CLI tools your steps assumed were
present, and non-deterministic, "spooky-action-at-a-distance" CI failures.

### b) Why split vet + test + lint into separate units?
Two reasons: **parallelism** and **isolation**.
- *Parallelism:* three independent jobs run concurrently on separate
runners, so wall-clock ≈ the slowest single job instead of the sum.
- *Isolation:* each unit gets its own status check, so a red ✗ tells you
*immediately* whether it's a vet, test, or lint problem.

In one combined job they run **serially in a single shell**, and the shell
stops at the first failing command — so a `go vet` failure aborts before
tests ever run, hiding whether the tests pass. You also can't require or
re-run them independently in branch protection. Splitting gives faster,
clearer, granular feedback.

### c) What attack does SHA pinning prevent? (incident name + date)
The **tj-actions/changed-files supply-chain compromise, March 2025**
(~March 14, 2025). Mutable references like `@v44` or `@v1` are **tags**, and
a tag can be silently re-pointed to a different commit. In the tj-actions
incident the attacker repointed the action's tags to a malicious commit that
dumped CI runner memory — leaking secrets into the build logs of *every*
repo that referenced the action by tag. Pinning to a **full 40-char commit
SHA** makes the reference **immutable**: even if the tag is moved, your
workflow keeps running the exact reviewed commit, so a hijacked tag can't
inject new code into your pipeline.

### d) What is `permissions:` and the principle behind it?
`permissions:` declares the scopes granted to the automatic `GITHUB_TOKEN`
for the workflow/job (e.g. `contents`, `pull-requests`, `packages` —
each `read`/`write`/`none`). The principle is **least privilege**: grant only
what the job actually needs. A build-and-test pipeline only needs to read the
code, so `contents: read` is enough. If a step or a compromised third-party
action turns malicious, a read-only token sharply limits the blast radius —
it can't push commits, cut releases, or alter issues/PRs. Starting from
`contents: read` and adding scopes narrowly (only when a step needs them) is
the safe default.

### e) (GitLab) stage vs job; what `dependencies:` adds over `stages:`
*(Answered for completeness even though I took the GitHub path.)*
A **job** is a single unit of work — one `script` executed by a runner.
A **stage** is a named group of jobs: all jobs in a stage run **in parallel**,
and stages themselves run **sequentially** — every job in stage N must
succeed before stage N+1 starts. So `stages:` controls **ordering**.
`dependencies:` is about **artifact flow**: it specifies which earlier jobs'
artifacts a job downloads, independent of ordering. With `dependencies: []`
a job pulls *no* artifacts (faster, cleaner), and combined with `needs:` you
can build a DAG where a job starts as soon as its specific dependencies
finish — instead of waiting for the whole previous stage. In short:
`stages:` = execution order; `dependencies:` = which artifacts get passed.

## Task 2 — Make It Fast and Smart

### Optimizations applied
- **Dependency cache** — enabled `actions/setup-go` caching
(`cache-dependency-path: app/go.mod`). It restores the Go module + build
cache between runs. Visible in the log as the "Restore cache" / "Save cache"
steps.
- **Build matrix** — `vet` and `test` now run against Go **1.23** and **1.24**
in parallel via `strategy.matrix` with `fail-fast: false`, so a failure on
one toolchain still reports the result for the other.
- **Path filter** — `on.pull_request.paths` restricts runs to changes under
`app/**` or `.github/workflows/**`; docs-only PRs (e.g. README) skip CI
entirely.
- **`ci-ok` aggregation job** — a single required check (`if: always()`,
`needs: [vet, test, lint]`) so the matrix can be changed freely without
re-editing branch protection.

### Timing (median of 5 runs)
| Scenario | Wall-clock |
|----------|-----------|
| Baseline (no cache, single Go 1.24, no path filter) | **39 s** |
| With cache | **38 s** |
| With cache + matrix | **52 s** |

**zero third-party dependencies** — `app/go.mod` has no `require` block and
there is no `go.sum` — so the module cache has nothing to store. The dominant
costs are runner provisioning, `actions/checkout`, and the Go toolchain
download, none of which `setup-go`'s module cache touches. The matrix row is
*higher* (52 s) because it runs four `vet`/`test` cells plus `lint`; even
though the cells run in parallel, each pays its own provisioning + toolchain
download, and wall-clock is bounded by the slowest cell plus the serial `lint`
job. On a dependency-heavy project the cache row would drop sharply; here it is
correctly boring.

### Design questions

**f) Why cache `go.sum`-keyed inputs and not build outputs?**
Inputs are deterministic and content-addressed: a given `go.sum` always maps to
the exact same module bytes, so a cache keyed on `hash(go.sum)` is safe — a hit
is guaranteed correct, and changing a dependency changes the key, which
naturally invalidates the cache. Build *outputs* depend on the compiler
version, build flags, and `GOOS`/`GOARCH`; subtle variation can yield stale or
mismatched artifacts that are silently wrong and hard to validate. Caching
inputs trades a cheap rebuild for a correctness guarantee; caching outputs
risks poisoning correctness for marginal speed.

**g) What does `fail-fast: false` change, and when do you want `true`?**
With `fail-fast: false`, one failing matrix cell does **not** cancel the
others — every combination runs to completion, so you can see *which* Go
version broke (essential for diagnosing toolchain-specific bugs). The default
`fail-fast: true` cancels all in-progress and pending cells the moment one
fails. You want `true` when the matrix is large and expensive and you only need
fast "something broke" feedback to block a merge and save CI minutes; `false`
when per-cell diagnostics matter — which is our case.

**h) Risk of an attacker writing a cache from a malicious PR that a protected
branch later reads?**
This is **cache poisoning**: a PR workflow can write a cache entry; if a run on
a protected branch later restored it, the attacker's tampered artifacts would
execute with the trust and permissions of `main`, enabling code execution or
secret exfiltration.
Binary file added submissions/protected_lab3.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 submissions/verified.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.