From f0191e4d2737bc306781a11f2c8f9daccc4e7c47 Mon Sep 17 00:00:00 2001 From: tdzdslippen Date: Mon, 8 Jun 2026 09:40:01 +0300 Subject: [PATCH 1/8] docs: add PR template Signed-off-by: tdzdslippen --- .github/pull_request_template.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/pull_request_template.md 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 From 790869ff22b0dfd0a190be8bfbb5b68fca4d4f35 Mon Sep 17 00:00:00 2001 From: tdzdslippen Date: Tue, 9 Jun 2026 12:40:10 +0300 Subject: [PATCH 2/8] docs: upstream moved while you worked Signed-off-by: tdzdslippen From 5416ce31d5460d82b0b42905125e7a79b2c8ea6c Mon Sep 17 00:00:00 2001 From: tdzdslippen Date: Sun, 14 Jun 2026 20:17:23 +0300 Subject: [PATCH 3/8] ci(lab3): add PR-gated vet/test/lint pipeline with matrix + cache Signed-off-by: tdzdslippen --- .github/workflows/ci.yml | 104 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 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..4eda37015 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +name: CI + +# Task 1.1(1) — trigger on push to main and on every PR targeting main. +# Task 2.3 — path filter: only run when the Go app or this workflow changes, +# so docs-only PRs (e.g. README, submissions/) don't burn CI minutes. +on: + push: + branches: [main] + paths: + - "app/**" + - ".github/workflows/ci.yml" + pull_request: + branches: [main] + paths: + - "app/**" + - ".github/workflows/ci.yml" + +# Task 1.1(5) — least privilege. The pipeline only reads the repo; it never +# writes contents, packages, or issues. Declared at workflow level so every +# job inherits it (and the default GITHUB_TOKEN is read-only). +permissions: + contents: read + +# Stop superseded runs on the same PR/branch from piling up. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# The QuickNotes module lives in app/, not the repo root — run every step there. +defaults: + run: + working-directory: app + +jobs: + # ── Unit 1: vet — Task 2.2 matrix over Go 1.23 + 1.24 ────────────── + vet: + name: vet + runs-on: ubuntu-24.04 # Task 1.1(3) — pinned LTS, never ubuntu-latest + strategy: + fail-fast: false # Task 2.2 — see every cell, don't cancel siblings + matrix: + go: ["1.23", "1.24"] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: ${{ matrix.go }} + # Task 2.1 — cache module + build cache. No go.sum (std-lib only), + # so key the cache on go.mod instead of the default **/go.sum. + cache-dependency-path: app/go.mod + - run: go vet ./... + + # ── Unit 2: test — Task 2.2 matrix over Go 1.23 + 1.24 ──────────── + test: + name: test + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + go: ["1.23", "1.24"] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: ${{ matrix.go }} + cache-dependency-path: app/go.mod + - run: go test -race -count=1 ./... + + # ── Unit 3: lint — golangci-lint pinned to v2.5.0 ───────────────── + lint: + name: lint + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@25e2cdc5eb1d7a04fdc45ff538f1a00e960ae128 # v8.0.0 + with: + version: v2.5.0 # Task 1.1(2) — linter pinned, never latest + working-directory: app + + # ── Aggregate gate ─────────────────────────────────────────────── + # A single, stable check name to mark "Required" in branch protection. + # Without this, the matrix produces vet (1.23) / vet (1.24) / … and you'd + # have to require each leg by name. This job fails if ANY unit failed. + ci-gate: + name: ci-gate + if: ${{ always() }} + needs: [vet, test, lint] + runs-on: ubuntu-24.04 + steps: + - name: Verify all required jobs succeeded + run: | + results="${{ needs.vet.result }} ${{ needs.test.result }} ${{ needs.lint.result }}" + echo "vet=${{ needs.vet.result }} test=${{ needs.test.result }} lint=${{ needs.lint.result }}" + for r in $results; do + if [ "$r" != "success" ]; then + echo "::error::a required job did not succeed (got '$r')" + exit 1 + fi + done + echo "all required jobs succeeded" From ff9227c977f45cad788234b0fdb52b6ee5fcd5fe Mon Sep 17 00:00:00 2001 From: tdzdslippen Date: Sun, 14 Jun 2026 20:26:35 +0300 Subject: [PATCH 4/8] ci(lab3): scope working-directory to Go jobs so ci-gate runs from repo root Signed-off-by: tdzdslippen --- .github/workflows/ci.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4eda37015..de258e229 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,16 +26,14 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -# The QuickNotes module lives in app/, not the repo root — run every step there. -defaults: - run: - working-directory: app - jobs: # ── Unit 1: vet — Task 2.2 matrix over Go 1.23 + 1.24 ────────────── vet: name: vet runs-on: ubuntu-24.04 # Task 1.1(3) — pinned LTS, never ubuntu-latest + defaults: + run: + working-directory: app # QuickNotes module lives in app/, not the root strategy: fail-fast: false # Task 2.2 — see every cell, don't cancel siblings matrix: @@ -54,6 +52,9 @@ jobs: test: name: test runs-on: ubuntu-24.04 + defaults: + run: + working-directory: app strategy: fail-fast: false matrix: @@ -82,9 +83,10 @@ jobs: working-directory: app # ── Aggregate gate ─────────────────────────────────────────────── - # A single, stable check name to mark "Required" in branch protection. - # Without this, the matrix produces vet (1.23) / vet (1.24) / … and you'd - # have to require each leg by name. This job fails if ANY unit failed. + # One stable check name to mark "Required" in branch protection. Without + # it the matrix produces vet (1.23) / vet (1.24) / … and you'd have to + # require each leg by name. Fails if ANY unit failed/was cancelled. + # No checkout here, so this job runs in the workspace root (no app/ cd). ci-gate: name: ci-gate if: ${{ always() }} @@ -93,9 +95,8 @@ jobs: steps: - name: Verify all required jobs succeeded run: | - results="${{ needs.vet.result }} ${{ needs.test.result }} ${{ needs.lint.result }}" echo "vet=${{ needs.vet.result }} test=${{ needs.test.result }} lint=${{ needs.lint.result }}" - for r in $results; do + for r in "${{ needs.vet.result }}" "${{ needs.test.result }}" "${{ needs.lint.result }}"; do if [ "$r" != "success" ]; then echo "::error::a required job did not succeed (got '$r')" exit 1 From a7905bc8b9f8721aa74eacacc1c93b3716cd25ab Mon Sep 17 00:00:00 2001 From: tdzdslippen Date: Sun, 14 Jun 2026 22:36:48 +0300 Subject: [PATCH 5/8] =?UTF-8?q?docs(lab3):=20CI/CD=20submission=20?= =?UTF-8?q?=E2=80=94=20PR=20gate,=20matrix,=20cache,=20perf=20investigatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: tdzdslippen --- submissions/lab3.md | 190 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 submissions/lab3.md diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..b9a56abfc --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,190 @@ +# Lab 3 — CI/CD: A PR-Gated Pipeline for QuickNotes + +**Chosen path: GitHub Actions.** I have working github.com access and SSH signing set up from Labs 1–2, so the default path is the natural fit. The pipeline lives in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml). + +> All run links and timings below are from real runs on my fork `tdzdslippen/DevOps-Intro`. Actions is enabled on the fork; the workflow triggers on push to `main` and on PRs targeting `main`. + +--- + +## Task 1 — The PR Gate + +### 1.1 What the pipeline does (requirements mapping) + +| Requirement | How `ci.yml` meets it | +|-------------|------------------------| +| Trigger on push to `main` + every PR to `main` | `on.push.branches: [main]` and `on.pull_request.branches: [main]` | +| Three independent units | Separate jobs `vet`, `test`, `lint` (run in parallel) | +| `go vet ./...` | `vet` job, `working-directory: app` | +| `go test -race -count=1 ./...` | `test` job | +| `golangci-lint run`, **pinned v2.5.0** | `lint` job → `golangci/golangci-lint-action` with `version: v2.5.0` | +| Pinned runtime (no `:latest`) | `runs-on: ubuntu-24.04` everywhere | +| Third-party actions pinned by 40-char SHA | every `uses:` is a full commit SHA + `# vX.Y.Z` comment | +| `permissions:` least privilege | workflow-level `permissions: { contents: read }` | +| Pipeline fails the PR if any unit fails | each unit fails independently; an aggregate `ci-gate` job fails if any of vet/test/lint failed | + +**SHA pins used (verified via `git ls-remote` against each action repo):** + +```text +actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 +actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 +golangci/golangci-lint-action@25e2cdc5eb1d7a04fdc45ff538f1a00e960ae128 # v8.0.0 +``` + +> Note: the lab's example SHA for `checkout@v4.2.2` (`b4ffde6…`) is stale; the real v4.2.2 tag points at `11bd719…`. I pinned the verified SHA, which is the whole point of the exercise. + +Two design choices worth calling out: + +- **`cache-dependency-path: app/go.mod`** — QuickNotes is std-lib only, so there is **no `go.sum`**. `setup-go`'s default cache key (`**/go.sum`) would resolve nothing, so I key the cache on `go.mod` instead. (More in Task 2.) +- **`ci-gate` aggregate job** — the matrix expands `vet`/`test` into `vet (1.23)`, `vet (1.24)`, … . Rather than mark five checks "Required" in branch protection, `ci-gate` (`needs: [vet, test, lint]`, `if: always()`) collapses them into one stable required check that fails if any unit failed or was cancelled. + +### 1.4 / 1.5 Iterate to green, then prove the gate blocks a bad commit + +**Green pipeline (all units pass):** +🟢 https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506521594 + +| Job | Result | Job time | +|-----|--------|---------:| +| `vet (1.23)` | ✅ success | 7 s | +| `vet (1.24)` | ✅ success | 12 s | +| `test (1.23)` | ✅ success | 28 s | +| `test (1.24)` | ✅ success | 25 s | +| `lint` | ✅ success | 13 s | +| `ci-gate` | ✅ success | 2 s | +| **Total wall-clock** | ✅ | **37 s** | + +> Honesty note: my *first* run failed — `ci-gate` had inherited a global `working-directory: app`, but that job has no `checkout`, so the shell couldn't `cd app` and the step died in 0 s before my script ran. Fix: scope `working-directory: app` to the three Go jobs only (commit `ff9227c`). That is the real "iterate to green" the lab asks for. + +**Deliberate breakage (Task 1.5).** I changed the expected note count in `TestHealth_ReportsCount` from `1` to `2` (commit `2e38db9`, *"deliberately break TestHealth_ReportsCount to prove the gate"*) and pushed: + +🔴 https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506990278 + +| Job | Result | +|-----|--------| +| `test (1.23)` | ❌ **failure** | +| `test (1.24)` | ❌ **failure** | +| `vet (1.23/1.24)` | ✅ success | +| `lint` | ✅ success | +| `ci-gate` | ❌ **failure** (a required job did not succeed) | + +The failing assertion in the log: + +```text +--- FAIL: TestHealth_ReportsCount + handlers_test.go:53: notes count: 1 +FAIL +FAIL quicknotes +``` + +Both `test` legs went red and `ci-gate` went red — with branch protection requiring `ci-gate`, this PR **cannot merge**. + +**Fix (revert).** I reverted the break (commit `bf66572`, `git revert`) and pushed; the pipeline is green again: +🟢 https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27507029572 + +### 1.6 Branch protection + +> ⚠️ Branch protection is configured in the GitHub UI by the repo owner (no API token available to me). On `tdzdslippen/DevOps-Intro` → **Settings → Branches → Add branch ruleset / rule for `main`**: +> - ☑️ Require a pull request before merging +> - ☑️ Require status checks to pass before merging → required check: **`ci-gate`** +> - ☑️ Require branches to be up to date before merging +> - ☑️ Require linear history (already proven active — a `--force-with-lease` to `main` during this lab was rejected with `protected branch hook declined`) +> +> Screenshot the rules page for the submission. (The force-push rejection during this lab is independent proof the protection is live.) + +### 1.2 Design questions + +**a) Why pin `ubuntu-24.04` instead of `ubuntu-latest`?** +`ubuntu-latest` is a moving label. GitHub repoints it to the next LTS on *their* schedule (22.04 → 24.04), which silently changes pre-installed tool versions, the default Go, glibc, and shell defaults. A pinned runner makes the environment reproducible: the same commit builds the same way months later, and a runner upgrade becomes an explicit, reviewable one-line PR instead of a surprise red build on a day you changed nothing. + +**b) Why split vet + test + lint into separate units?** +Independent jobs run **in parallel** (lower wall-clock), **fail independently** (you see at a glance whether it's a vet, a test, or a lint problem instead of one opaque red blob), have isolated logs, and can be cached and required individually. One combined job would serialize the three, and with `set -e` the first failure masks the rest — if `vet` fails you'd never learn the tests also fail. + +**c) What attack does SHA pinning prevent? (incident)** +A mutable reference like `@v4` or `@main` is resolved at run time to whatever commit the tag currently points to. If an attacker compromises the action's repo and **moves the tag**, every workflow using `@v4` executes attacker code with that workflow's `GITHUB_TOKEN` and secrets. A full 40-char commit SHA is content-addressed and immutable, so a moved tag can't change what runs. This is exactly the **tj-actions/changed-files supply-chain compromise (March 2025)**: attackers retroactively rewrote many version tags to point at a malicious commit that exfiltrated CI secrets into the build logs. Repos pinned by SHA were unaffected. + +**d) What is `permissions:` and the principle behind it?** +`permissions:` sets the scopes of the auto-provisioned `GITHUB_TOKEN`. Setting `contents: read` (and nothing else) is the **principle of least privilege**: grant only the access the job actually needs. The default token can be broad; if a step — or a compromised third-party action — is tricked, a least-privileged token caps the blast radius (it can't push code, cut releases, or comment as the bot). Start at `contents: read` and widen one scope at a time only when a specific step proves it needs it. + +**e) GitLab CI: stage vs job; what does `dependencies:` do that `stages:` doesn't?** +(GitHub path, answered conceptually.) A **job** is one unit of work (a script on a runner). A **stage** is an ordered group: all jobs in a stage run in parallel, and stages run sequentially — stage *N+1* starts only after every job in stage *N* passes. `stages:` therefore defines **run order** and, by default, that later jobs download all earlier stages' artifacts. `dependencies:` overrides **only the artifact graph** — a job can declare it pulls artifacts from just the `build` job, or `dependencies: []` to download none (faster), independent of stage ordering. (And `needs:` goes further, building a DAG that can break strict stage ordering for earlier starts.) + +--- + +## Task 2 — Make It Fast and Smart + +### 2.1 Caching +`setup-go` caches both the **module cache** and the **build cache**, keyed (here) on `app/go.mod`. Because QuickNotes has zero third-party dependencies, the *module* cache is nearly empty — the real win is the **build cache** reused across runs, which avoids recompiling the standard library and the race-instrumented test binary. Cache hits are visible in each `Setup Go` step log. + +### 2.2 Matrix +`vet` and `test` run against **Go 1.23 and 1.24 in parallel**, with `fail-fast: false` so a failure in one cell doesn't cancel the other — you see exactly which toolchain broke. `lint` stays single-version (1.24); linting twice adds no signal. + +### 2.3 Path filter +`on.*.paths: ["app/**", ".github/workflows/ci.yml"]` — the pipeline runs **only** when the app or the workflow itself changes. A docs-only change (README, `submissions/**`) skips CI entirely and burns 0 runner minutes. + +### 2.4 Timing table (real runs) + +| Scenario | Wall-clock | Run | +|----------|-----------:|-----| +| Baseline — single Go 1.24, **no cache**, no path filter | **39 s** | [27506614357](https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506614357) | +| With cache — single Go 1.24 | **35 s** | [27506696369](https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506696369) | +| With cache + matrix (1.23 + 1.24) | **37 s** | [27506521594](https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506521594) | + +Measured with a throwaway `measure` workflow toggling each optimization, then removed. A cleaner signal than run-level wall-clock (which carries ~5–10 s of variable queue time) is the **cold-vs-warm cache** comparison of the *same* matrix workflow: + +| cache + matrix | wall-clock | +|----------------|-----------:| +| cold cache (first run, cache write) | 45 s | +| warm cache (cache hit) | 37 s | + +**Reading the numbers.** Caching saves ~8 s on the matrix path (45 → 37) and ~4 s single-version (39 → 35) — all from the **build cache**, not module download (there are no modules to download). The matrix adds ~0 wall-clock (35 → 37) because the legs run in parallel: two Go versions cost the same wall-clock as one, just more runner-minutes. The honest caveat: these are single representative samples; the lab is right that you'd take a median of 3–5 to remove runner noise. For a std-lib app the differences are small because the dominant cost isn't dependencies — see the bonus. + +### 2.5 Design questions + +**f) Why cache `go.sum`-keyed inputs and not build outputs?** +Inputs (the modules pinned by `go.sum`) are **deterministic and content-addressed**: the same `go.sum` always means the same module set, so a key derived from it is *correct* — a cache hit provably matches a fresh download. Build **outputs** (compiled archives) depend on toolchain version, build flags, OS, CGO, even paths/timestamps; caching them risks serving a stale or environment-mismatched artifact that differs from a clean build — the classic "works from cache, breaks from scratch" bug. So you cache deterministic inputs and let the compiler regenerate outputs. (QuickNotes caveat: no `go.sum`, so I key on `go.mod`; the build cache `setup-go` keeps is still safe because its key includes the OS + Go version + that hash.) + +**g) What does `fail-fast: false` change, and when do you want `true`?** +The matrix default `fail-fast: true` cancels all in-flight cells the moment one fails. `fail-fast: false` lets every cell finish, so you can see *which* combinations broke — essential when chasing a version-specific bug (1.23 only? both?). You want `fail-fast: true` when cells are expensive and you only need a single green/red signal: a big cross-OS/arch matrix where any one failure already blocks the merge and you'd rather save the minutes than get the full breakdown. + +**h) Risk of an attacker writing a cache from a malicious PR that protected branches later read?** +A fork PR can run a job that **populates the Actions cache with poisoned content** (a tampered build cache). If a later run on a protected branch restored that cache by key, it could compile/execute attacker-influenced bytes — **cache poisoning**. GitHub mitigates this with **cache scope isolation**: caches are partitioned by branch and a base/protected branch will not restore a cache *created by* a PR branch — a PR's writes are confined to the PR's own scope and can't overwrite the base branch's caches (see GitHub Docs, *"Caching dependencies → Restrictions for accessing a cache"*). Defense-in-depth: key caches on content hashes (`go.sum`/`go.mod`) so a poisoned blob can't masquerade under a legitimate key, and treat fork PRs as untrusted (use `pull_request`, never `pull_request_target`, for untrusted code). + +--- + +## Bonus Task — Pipeline Performance Investigation + +**Target ≤ 90 s: HIT** — the full green pipeline runs in **37 s** wall-clock. + +### B.1 Profile (per-step, from the green run) +For a `test` leg: runner start `Set up job` ≈ 2–4 s → `checkout` ≈ 1 s → `setup-go` (warm cache) ≈ 1–5 s → **`go test -race` ≈ 17–22 s (the work)** → post/cleanup ≈ 1 s. `vet` legs: the same prologue + `go vet` ≈ 0–5 s. `lint`: `golangci-lint-action` ≈ 7 s. So **the race-instrumented test compile-and-run dominates**, and the fixed runner/toolchain prologue is the second chunk. + +### B.2 Optimizations applied (≥ 3 beyond Task 2) +1. **Build cache reuse** (key on `go.mod`) — the single biggest saver: cold 45 s → warm 37 s. +2. **`concurrency: cancel-in-progress`** — a new push to the same ref cancels the superseded run, freeing the runner and giving feedback on the latest commit instead of queueing stale work. +3. **Path filter** (Task 2.3) — docs-only changes skip the whole pipeline (≈ 37 s → 0 s for those PRs). +4. **`ci-gate` aggregation** instead of five required checks — simpler branch protection, ~2 s and no real runner cost. +5. **Lint kept single-version** — not matrixed; avoids a redundant second lint with no added signal. + +### B.3 Before / after + +| Optimization | Before | After | Saving | +|--------------|-------:|------:|-------:| +| Build cache (matrix, cold → warm) | 45 s | 37 s | −8 s | +| Build cache (single-version path) | 39 s | 35 s | −4 s | +| Path filter on a docs-only PR | ~37 s | 0 s (skipped) | −37 s | +| **Full pipeline wall-clock** | **45 s** | **37 s** | **−8 s** | + +### B.4 Bottleneck analysis +The single dominant remaining step is **`go test -race -count=1` (~17–22 s)** — building and running the race-instrumented binary — followed by the fixed runner-start + `setup-go` prologue. To shorten it you'd have to change *QuickNotes itself*, not the pipeline: the app is already dependency-free, so the lever is the test/compile surface — e.g. run `-race` only on a nightly or labeled build (the std-lib app has little real concurrency to instrument), split fast unit tests from the file-IO-heavy ones (each test does `t.TempDir()` + JSON disk writes), or trim per-test setup. Below the toolchain-install + compile floor there's nothing left to cut without rewriting tests. I'd **stop optimizing at ≤ 60–90 s**: we're already at 37 s, comfortably under the threshold where a developer stays in flow waiting for the check, so further seconds cost more engineering than they return — past that point reliability and signal quality matter more than raw speed. + +--- + +## Evidence index + +| Item | Link / ref | +|------|-----------| +| Workflow | [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) | +| Green pipeline (cache + matrix) | https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506521594 | +| Red pipeline (deliberate break, gate blocks) | https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506990278 | +| Green again after revert | https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27507029572 | +| Break commit | `2e38db9` · Fix (revert) commit | `bf66572` | +| Branch protection | configure + screenshot in GitHub UI (see 1.6); force-push to `main` was rejected (`protected branch hook declined`) — protection is live | From 7ac0086e8a04d00abbd3f3aec7202f05c5414180 Mon Sep 17 00:00:00 2001 From: tdzdslippen Date: Sun, 14 Jun 2026 23:28:25 +0300 Subject: [PATCH 6/8] =?UTF-8?q?docs(lab3):=20move=20branch-protection=20sc?= =?UTF-8?q?reenshot=20to=20=C2=A71.7=20Document=20per=20lab=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: tdzdslippen --- submissions/lab3.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/submissions/lab3.md b/submissions/lab3.md index b9a56abfc..20538c2ca 100644 --- a/submissions/lab3.md +++ b/submissions/lab3.md @@ -87,8 +87,8 @@ Both `test` legs went red and `ci-gate` went red — with branch protection requ > - ☑️ Require status checks to pass before merging → required check: **`ci-gate`** > - ☑️ Require branches to be up to date before merging > - ☑️ Require linear history (already proven active — a `--force-with-lease` to `main` during this lab was rejected with `protected branch hook declined`) -> -> Screenshot the rules page for the submission. (The force-push rejection during this lab is independent proof the protection is live.) + +Independent proof the protection is already live: a `--force-with-lease` to `main` during this lab was rejected by the server with `protected branch hook declined`. The required-rules **screenshot** is in [§1.7](#17-document) below. ### 1.2 Design questions @@ -107,6 +107,23 @@ A mutable reference like `@v4` or `@main` is resolved at run time to whatever co **e) GitLab CI: stage vs job; what does `dependencies:` do that `stages:` doesn't?** (GitHub path, answered conceptually.) A **job** is one unit of work (a script on a runner). A **stage** is an ordered group: all jobs in a stage run in parallel, and stages run sequentially — stage *N+1* starts only after every job in stage *N* passes. `stages:` therefore defines **run order** and, by default, that later jobs download all earlier stages' artifacts. `dependencies:` overrides **only the artifact graph** — a job can declare it pulls artifacts from just the `build` job, or `dependencies: []` to download none (faster), independent of stage ordering. (And `needs:` goes further, building a DAG that can break strict stage ordering for earlier starts.) +### 1.7 Document + +The lab's documentation checklist and where each item lives: + +| Required item (1.7) | Where | +|---------------------|-------| +| Which path picked + why | top of this file — **GitHub Actions** | +| Link to a green CI run | §1.4/1.5 → 🟢 [run 27506521594](https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506521594) | +| Screenshot **or log** of the failed run + fix commit | §1.4/1.5 → failing `--- FAIL` log + 🔴 [run 27506990278](https://github.com/tdzdslippen/DevOps-Intro/actions/runs/27506990278); break `2e38db9`, fix `bf66572` | +| **Branch-protection screenshot** | below ⬇️ | +| Written answers to 5 design questions | §1.2 | + +**Branch-protection screenshot:** + +> _Pending: `branch-protection.png` — screenshot of the `main` rule (Settings → Branches), all four boxes ticked and required check `ci-gate`. Will be inserted here once the rule is enabled on the fork._ +> + --- ## Task 2 — Make It Fast and Smart From 1a33a36d7787ea20f4501cf19b097f5d2edd9db0 Mon Sep 17 00:00:00 2001 From: tdzdslippen Date: Sun, 14 Jun 2026 23:36:40 +0300 Subject: [PATCH 7/8] =?UTF-8?q?docs(lab3):=20add=20branch-protection=20scr?= =?UTF-8?q?eenshots=20to=20=C2=A71.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: tdzdslippen --- submissions/branch-protection-1.png | Bin 0 -> 361558 bytes submissions/branch-protection-2.png | Bin 0 -> 335499 bytes submissions/branch-protection-list.png | Bin 0 -> 40309 bytes submissions/lab3.md | 16 +++++++++++++--- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 submissions/branch-protection-1.png create mode 100644 submissions/branch-protection-2.png create mode 100644 submissions/branch-protection-list.png diff --git a/submissions/branch-protection-1.png b/submissions/branch-protection-1.png new file mode 100644 index 0000000000000000000000000000000000000000..9d43fb90f491d26a0f4fe0eda50a6fcf0e747038 GIT binary patch literal 361558 zcmeGEbySq!_6Lq5DIg%NbO=ZzDGkz+(%s$NsR$xS=g=+PATfXf(nB+JcS<*WAKr2A z{ap3Eerx@{e|^_wt(kddo_)@F&OZC>^V&s-f}8{z3IPfX3=Eo-q^Kec3?elQ43f}8 zMBo>#w+GK)U>?a>h=?diiHMLXINF(7SewAWNQT6zA*m~M>Z$%;5Beq6K;(vQX(Wc|7c$0fL0sJ_>#$QGYmMeUz|s*!oy( zTwHYhr{;IMbF(lXD#2SBqnBoPevF#0qO@UPun-(T3QS$^&&goiV}$5>;ryS!Z@L)e zE5IrXQkOs^4*c>ugiZO2RLF?(xnsJl_9OhIPMFBC3vUfX7=qbIvsF~q40K{~)Rbqh zD&P$TKlnI0(V&v2p;n+(2RIGKUG7wqIWPeoWEiVyVqNECOk1ov@e)RK^#{ohH4lUwD#{0RLz0p{M!LuGTT(_k z89-pLmtGvEsOt)nQm!Bc3R^#PXn00L(KMgOqC=EZ9dIGi+wRfy^1}&PDONX}(Upx{ z7j@^OJ}*k0@2kg01Lu_jq_LOtIFkoNaRPa%v`Z)3M&Sra4k3?~$kDs-X+(vJA2}xTFA+HyK{$b20^7UO7CeB_kESQkwpwh&P`)c5}Gj+?;-C zc}}C=oh(d5GY~=iBsm=kKmRo;bM%^6N5OS_Z`FC`8#=!qU@{_mVHo`f2*noqtX;56 z{8EXaQl6%u#}X(>~KKP10%d04d4{w`%} z3ugH0Dwb|?0lB@N?9ocAmsonV@!&i7A5U19{M_4vRbY&|0#qjN_Aefs^0o!YxC;Wy z5+z1j{tOWd?gM!{Sr((M|J8!7`^%=wuUlbN4iNz_o2RkU9#1{QK!zgk22RJczpo_M z4M*5j5d17CXd$?URQDEC`bBULZ7h=&(vId2w+FkvxX^5Gs@COrb)#zfodw7%8LEMxi6z=W`1)e;GJ<(T3S4USkp^LjWK67)TXE$n(O?%yJZ%JX?N%wbm z`43Z3`O#Yi+|H$5UZ++WwCRNsz^r4S;#7L$2GcXcpRZ=L)yb1SfpgM_TXsOSZACo~ z5PFP4AxtC*i=BCvWS`pGs?37j2Pc*R&wykl#61CD`7y%-s5nZW*YVN%@AeN4Rb9r86ujKu#ES5Me{H z`ca9fTc*o~)G>u8v5kaY2LF@RQ5I^`kKFp!VWDD%-ReBlZKg&Y>a?mMV}_r8aO-tU zRWR*P%C$dziR$K0{}Mi;6X6`ko3k07xwGg8&o#R*uAjf#^87>gx8z^b7Niz1zhkmK zR6-UDRQ%@pjr`lwb0(L^bC@Bb)I*>R^!biwhIkH$4#*C04j2dU6`#&OOo&iK9ZQ`) zvr~K)BHu%a8#Nx~8WkQ>8p$I`+GS~sUCGA%Aw?!W616*bC3VGUg?=S}WdKL^BUMKX zmK3I9o>Kgq+BXEW3D1dR2b38LImVI+B>J-;+3|0z3d}yc71)iAj`of6jb@FtTRM&P zjGh*zs(EFczR}T|l}jy3%PLjSD{L0I3B0s9;bJG~|F|Y0owqs^KU`>$Z+>P$%Kn5P zoPZB4^E=TelutaL=n0rNaT=&nMbagiGZKa|2APK%*=a$lAZ^*^HS2zq{x%R7hgbCz z&FpH58poP=^WD*gj1!sU9GhI?&y1tG8DN#G*i%Zx?<{8aPhr1%f9L%ER6H2{O{ZpX zeoU@XR+U!1O1_qw7F3K&*bQX+e(%k!EA{tk!^1@GK9jziKEgivwy+?FppYO&5>?)v zp7Jx#wv+2)QO^!fujA5fpJB62q%E`UvcWQFi7@vxU7#G59i8-cjD!HSv5x(y|(|5&I_L#mL0K%U<`IgBQzk*g+u?Arrp+Sngj1eN&t}eg&c?v- z#kgUf*r$&DcpT|kle|^UUZ?o|lHwAq54{mU0 z6N@^L_*e`pmssSloZn znccmdou6!$w(dL7(nQo$33m15Cp6+0XE&Wrl$bM}^WJBi+ZA9)S5EI2*lo^h_MU$| zAMCs4`>JKvSDw2zg#p3@k@lnUOS&V!JJXimW7rFUVIX^%@iK!$ftN*~obQKkq=tgH zf~BaxM>Xz6WI>DDf5v5B;%oqNlpGy2nJDeWSgG`?9*T^~QvX z(Z0C4xFp3ErSLOmN0k-u^X<>HZzQrphTaU}4#^vecl3pE%yI+v7rZm&L=AIra>rg`-~`)%x- zTvVNT?uJE!+hxzO+0(hFMXWq*K5Wu#jqTbF_2(+8lH+;BXOmHryp>)~SWc%a(N0QE z7}C{LlhGqFpJ?QhXQ*ou@T8{m-Q_gca<%ZNrkLySPIGAkJsjAyGE$c)8osl(@a z(9kje@qI^2{L8r5cyA2~^~3FfY+t6UdN+&9;c*k&UQQ+&rUZtTif?XWW3Nl%5a>xu z2pS7*Ijmd`w(6{?OX?N26dNoUT}ljXyfoZ3+IT`;dZ9kF0}L&?i+UAi;Kj?1?RmS$ zwqGiT?2LA)vt_B01#Ix_nro=srFC*uOg2@Q#;;4{YpJYx&rq)twssMxrEC3W$tQ<& zag|&eroauJ`HK(}4)af0c}{9QrM-=Cu3V!$qqdW6GfI{CJ1RRq&CD+IpKc7Uk}=9L zMoFsqzT7@uR&XFzmNDxKrn6ugxzAW@|o_w)Yfzat0 zflsv0P0l~$!n(YYy(ZP~CijWgW!L$5bDYh<43dt<6vvxV1saiMQAibABz~rTEYfU)fy7O^AE=CQmG!6W{(e=Qy{iK%gfXjAP zrhd{_+I@R+H)Xoaci`^kD7D@V*)4S6VvZhc=iYLNF)esn-B?5F^Y}-`s=-bhZv=dV zE?>Oh?Qq2c@~+gTQcV8XKuS7?3!;mzufVNGC+cSCfR31-Ntd3?XYJB5>|n}eDBc6oV0`I}dlil<|zR?Bte zc5YryUS7H0Ph0Hh{U59jQ4{+^d0{fB9<=(ei*LD)U6hxR6|&io!rmblK_Yz+_OvY* z(*zn&_iSxp%iRcJR292LyQX&eE&|n_e!Y%Iu-#?nDOPR{b!q5TN4`JXR z5WpY+R}X-fzyrd6U5h_>4g>ec@31g1!4@#^f6>SRpZ9-}!0W!vpPz8iK`=hMDd1Dd$kD{a*2&z?*-7tc2lxTmUQ)ve1_qDn{`Ejg z@!1cc{|O6ab!T;186G1$8zuu|J3|vDcN_creqi|Ad4NkB6K4Z5cN=S4Cmwfxia#iL zfb098nJLKrAaSExABS69EZV;#Y+xOzCRlLbnD+k{zp^w|7gm=#r~g7|KrwwH&t;maTKw$0S0vz z`1gYS)%ZVe{?(9=`F`&I!HYi`{l~8Wrv*^>nE$nE0w@%-lQFB%810w_@B`U1!{$OVo$$xZoy!Ajt0!M+CLPsbPosLXDb3&}6 z)c?ctk0>9&-Z;28gszVp^&e~wV_Hz)DbQvOH-41Iw0Nnpe z#ahr}%*uPVl9pU{j%jj-i30nO%pc}AZ;`M8-tg3l8GY1`g(d!PfTS6C}^`yMgDG=3;!kM{ee^%v>^D00F;XypH}bOFev|1iR1$t0M7kk~g>D(e8PlxtP*N;U6)?B z;_3Ysf!`C#j2;&j7B=Jk(!+7SUTw8|1R_lN->WBtjw>mpZ+HT#Q7l%kV8FD#Tg6Xe zb@|A5-U_FTVk*U2kDBf^=W%%0TlPhj$F$G9zQoNA$}ax6s{~dlPcBaU6G5=6-$2@| z;?abW$=&Turp2Qb?+1G-+;>D3!9VSaNH=&#S^aMURE@5?(OS06Ql|YN6Zs{MPq50T zxtQs)W{pDcQE*p|#6sI@vmXqr>3VQm^I$uc!kiVRc-c;{{Un=RUa<8{EC%UWwzH)P znrJhR|+jPt$R<%WW0HyYDQ^n<&9)J@Xux zAv$r^oCuvt;S+*0!siMZ{>5x(iEj~5>*tz3-@LDMcW{?Rfy#ufJilk;mUB7V&y4Iu z7{*Kdte)vDdf5|4FZ~V?Z8b01Hah4N!3E^%pc8eahraZOPee!oW=1752Sj5QB>*`} z6c&5Sx0kW5i5Dz)#d^BmN*jh*ca2Vi@kwmC_eX>;YPnBR`hTHE0Qs`k|5iWX zvOOMGKy4FqR$x-{toRKWv@==N$<5nFivtYwenMjcCm^I94!b99zIv(fO?J@r`EtZz zC#j$DqD;AY13b{bblNV(f6E~Z>>_ME1xsclaqRZZQs`7~mRRVwh%j2(`^Ts1)0lf0 z5K@dlUFi+0EGQf;bWTxOCf58@4ISy|KVlvSxkSdJVZ z8KW9zCerZK4`qy3FS(z@uj;z6q={!lLfbYu+2UZeo6{az4pF63{!A83rBJm3k_jr&1#1ehrsvjJL4(GVH-uEn-tfC} zl=sfmA3aO2s0qz`Yc;#Y?ltOp;*0K*<%=gBM~4d<@M1;=V;W?<$^JfEtii^8Izf9U=Y&?QQKc$yckQIn;4BrGJ8Or_q^*A3P#HS!VKvrJlj7Jzt61||WxMKT8AGru zLC=SZ)bF~YShIdum))W)YR}A*q#;p&n}FTg+~@Z0_R5Yv(R!Zw#IsDb1Z1VhX+1%9 zb++#&8_JHg$!5-s-wWy=Uv}%5WDp40w7!#RvQL#+Q`79=4hGjm4IK z`L$5e=VT0;qkD51&8#P|zNOP99q&y=T)Oqx%|9!%fM~~v?z=osfk*qvoe^C@0+SY< zTOx{0i8VT16x|i*$v}RZ?-Yt8JY$QST~HV)m)u^%c4-d6^XhNjfEF&5mGkt=n(;pB zAFuY>2M=s1gZ(M(7eSL@#q1AI`1gnC2;S9>WlLaqtj4Ly{#Y+&S+{D`UM6fG7Sc=? z@L9b^h1BtAp6Yt z|02?pFi`yCEZHL)na`q?#r*C%z9OG$o4{wepKCRY4N|Q}I8t=O?F_lTSP#v4i;O35)Jfn=V-i2 zjd{~SI^x3_-A&)D4|J9lI5D9-m8@9Kf{h(SExxtj8#vh~g1VlkI|ep~-w7K-Yd0L? zh2N2c5InD4x0|fYZQ}9_%wUn~wFT9=^N4|)!h!0_&Ay*tH^19FV_DOsr8<@F zAP+0+spP(vU7S=5HA4Rq=(a0Ec)0$Eqts$Zyc)}OBa%D|_q{LG=gR%lL64qpdD&c+w56T)bJ4H7JsNPUK^!{6LX(;C zR&tx)mP+?bzxax@)_uBlnrGw*93~x(-5vOy*T-J0WkYUr9%^z{V{~~p)n>y3iwBv8Q15dqxWsvqYt!4OySAflWNT*K|ZttNqj(R#8pcA=gTU-)n{f zny89QleR&s?b3fe|Dm>dYjBZn2=9>wqwiT2*POe^Uj4f6yb3QI5`oMCiLd--15L)fBP%bQPBG`R zQ_Kqtt3grv@v#hiRoku&Bxkinpo$! zEjF7Y`%!6;XNc6*CgNwww>JHYb)U>fT=!A^~@aM++`aY0lGBFu||yNsLB z45ehw5-}w*gt(nA;v=|;FPFpEM78+EETgbjGDgn!r)hxGHPx<&z-Jy1(Dc+wlZfHE zWja!C7NrJVFB|RN^Zp%h`pwCDy5i2D2pn8YJvl&@>5yk4f$cOenFHVwEhfu(-FRlb zw5}zFcRz0xqS1TIF;p^*HhdR=^i3|X?k$x2ou2-;i)Q{}c|oh5XZ>fTH)s|P?Wt7m?V&7#(SGtB3)+ZVB)DBFCw zCB0qXvCh8!1C{wv)G5BsN(v6a_%ih7diGc&bNYjF()o|p2Zu|c%t7&E4_aCSt%B~v z**ii6l1eTvDG|Kp+;2EGtrTTVa?cS^ugCK{ORs9Y&TQ%RRf^SLv`iLTV1TywL@q;zmeD5mC0=~Tu?ZRBsL~TI4R(x#Xhl7BESFT#r z>!tK=^VYnG(5(LOBfW8j1^EKQ93C9x%-Lg?i!%LU9#et@b7E>eMjo$cH9==hdz2%E z82TBJt&H`idZJ^$VPv4P%3P2MALO*eFt@D6C5yVQ#{M$HS6CR?Mz+SOQ_gBAIym7` zlbgBys}UrN)~{OEu-J1R&ZIR11pI;o&qj_&m8WWLVpe@=4C~W3Pj?4ey|Tr7h^eU( z<5Q*#4L?g=tnVgtOc@-JqCK>NaV(R9Dp#&7W;Sc@ob9TC{h(|&6~+>RJg1!QV*d@& zAHX|i3xz+D*@!8es^9LQM(A^a>e;DWyE~+N?P+*iS9CVTh%86JVo&>BboBTL`d+`( zsI^W~Dae_wPTy$mQeG0YqdNkNX{pTE3^Vp8^FmMJRpqQ2B8I~uwYyI}Pqr*hGW8>- z%cAHGt63G5ggvgfzMPEDIj*O=U8>3JBK>ucsAjY)gz=C#`TdZI?6V&^>RD zADKw2pR;>B@S|$1#wF?H*JpT?s$1KwsEKE?BhVdl%?p(aqQi(ddc12{uVyQ_o>UWG z-KgtQ&JHx+oDwGSc~rWPj$Zk@J}VW*ZmblbL8Ly`WpSB0E*F7bwX(?z8V#4CIh>GZ z5`PlBJ;yfKgmXcFkjYG-B>il4JY0DbP>n$Cijd<%tDR%NpaebTzWmXqF}w=u@}Yu9 zpsvPhzL27+r-SaEOqCgZ59cVM(VPH|jD{Mk5@&W{+%-VLDpz~Iy?riR^7Jj;;W6#Z z8a00$@iT|rz^4T+GY*G~!BuT<5$(Vnul5?xboX~Eoi{7X{Xky~#jIz0hjdrnye}c%L#4WKPX0{V9`47GZp%J>fF-`+(`EHUp z@ykwS@9L&mS_K!r=h&#}da9RgMgpQUb)sC~jbe#~!oakYOHONh>jRALa)Ymo_SFMK zc7iyUNuL#S9nJl)pFV6z?}(Ni3 za;5vC#;jRXNuA*on48@Gz})+2-h#v1fZp)Y5`H4@a+XkGIoW)wv zh^Sj3+ZYU07@1y@@3WqMIlU@(KfpH-en-ySv@rd~16Y`8+eX>FFGoyXG2`3md1Df@ z4KA?sleqER^b&c)f?dN z=XhsiJXyJ_Sm{i1Qt84NI6Pg;9|69oQYJr>{(;g;3oP`=JFu;0!V zS}n~4R^A#dzy^>9|fb(dedx4+-ti z-M}SBP_ERV&P(Hy3<=V>qO!dmO^HYyV5Ja9{TKi{y1&Nv=6YlOdHms&Ravh_o|3KE z(-($c;<9?u;y+rA&Z)X0QlsEAF>H^Q07*11tipL7)Dt+;7DHLNVI*wdIf{Je3e|YO zALB-0g7yG_Yb8-uuik@HWtw!Qr2{R7x`0(pAt{?6ynzq%H#=*|4V^fm5Q~5i3E72O zrTikDV8K7IWFs#w$O^IZgC1Qg;E3ujpq|A>_{pX5SZF~nKpQ`?Ts~#(ay)+?pFv$; z-VuC>K)u&|#pH38>ZW}}{CwVF4r7&gw;{VN94X>~~u zI4-I}qLA20##|eT6ZBet_v)L*)_4JNMQ(`GoO%#o1!&b-N5#kpKCQqj6KldcB3_<(the_L#vUz{!)~wdEaST7vhpAMTbvQNKvT)$St>urjSH(s19^& z&ReFetoag@;sfC>^{8Rn2EfYuJYdUHhmkV#d!0V)$F^wQZx|o71uZcSE!{lo#MbV! z2teNFcsjbp7(I4jy3?{|{&6X|sY@Z1R&Orm*=}>i0#h(E3(ym3m%^$z>P^S1r z{i0U#vEl*FZp6xsRE$n2-~>{fs_oyIZ)0Xzgi6&~QJJlt^@sRMT^|%!)EcPctQsw~=O$f(8Gh6N_E0H(ihG;VFN>TaA$Es^yaM)AbyGzW? z&bzbnf;KVO;evAU87Uk8N|Vxz=_@e$OtS zv)O%3J67?O&ZI&>zwQIPl1XC~JN8{c`?<{mbL&gy(z8sgc7kBprBgRpkL1akT*JFrCEO$DBGvr&ss)LN>OD-i#3$i`cl_<;CzWg zO{rKG)h4&6GK@siVdeq<;LdELyzBlP@%W^311xs#>$qr>-WJ!Lf)9Eyog5;tloiUS zVp=wInlkOf`|sQYd*kS7^t>tD{Qk2J$ z0x)GN@j@OrQmLEonp4~jvAvYWtz7r}W$n%&`JO z9>3>^>}w62H`C>c?sD1JDOKu}%VMm1{!>f^uns7IU{WTHP4D=An z*Hf`n*zRg_?^Y`<^E3f*uI?`fdyvm+rPp$DcGH~IX`*o0dW;$rCcPxMni1%uWol}! zI}?i_JR4)+Py>R2Yo(LdbnVM%GSznTy1GwTM{DLn@{IOK^4{$k$17`RpWd39>3?Gu zVz+MYIh%`` z>dJMOW+u%h^`3;X$DoxRnzpowd^^3(xf^Q0yRY+O$h#gm^0h~@J~dtKNphc5_f^aC z+?5+J_q0BMdlkC&zO_Bsu8GO#@)()eDd$0I*_A9;qiOeR{cp*pj2k3-Q)HCC5(Wq4 zm4?=K+WZs>N(t~fk7UcoZ@`*WnGHl~b`q~^-qDveF<@LeH|&DC!*FO7MaN6#7eGYH z1vMtUs@(~Ke&%!TBMf5O*P3cOXUexD@@VmZ)fQY!XpQG>+U{QwAX1pYH#`yJ18-|ddb+yo8G&PaX;1_sPNfL?WjspKzOx24J?dUyOsm*f z7SBd+OLZD^)+#u5`a6#srV{blP60JJajB0gb6$gWVSekqIY?fc0qKgOT#Z$l{hCq? zPaW?EreL6&_gGJ(?2Tu1O2Z4n{6%I(C0XP)EVdmwDN}yBZ_sw+4IK_H=~^4(v7We63tvi5jGogGuQU%FcElrsXQ{k;%QM82^-jT~MOsh}M)Mr|#c!40 zkKZd~=zkq#&^*w|b)@39U-0K($E_+9OSr9G0Vz7=3(GlVw#o zY%FgI6sazbs|HdVjnF!$U{}5(kw$Nqw#?OBpJ~;-t}>Ms8M&szty#LLodb&;i4sbR ztwT=*+BGX~hu$J6mT0~GBVJjK=Pob=J69^EZy-o+B%?r=ZEEV7BCY;gE+BHCqmIy6w``@;`4um55YKW- zv0uv&`5^`iZE5MQXR6y9U{_N-f9`Q0wp()h{u}37(gIYlSI_VE>DKWq8*lc{`&=ju zrwcbA#BsH!n=*or!Ysu@Rb8w&twt`Dr>CjjNr#37*m5hhN;wY8%R1XAo+z_vQt7^= zY_7!{*h_QKfH3U!uBS6cqu|nex=E9eyljn&?&fWyO3ii6M6QoJ4Ai{71ye~RVZ`_jhYrV5Mgi`&<5SfG^O^@;1t zRjs|P@;fZ0eAxsMbnbR;jpNPBL8##z+OBfhRt*TqcHM>4?HNE~-A{GRs8}+$e@L�_I973-rWrWL;9Bz)CY) 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