|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8" /> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| 6 | + <title>JOSA OpenLab, Week 05</title> |
| 7 | + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/> |
| 8 | + <style> |
| 9 | + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| 10 | + |
| 11 | + :root { |
| 12 | + --bg: #0d1117; |
| 13 | + --border: #21262d; |
| 14 | + --accent: #00b4d8; |
| 15 | + --red: #ff7b72; |
| 16 | + --green: #3fb950; |
| 17 | + --amber: #f59e0b; |
| 18 | + --text: #e6edf3; |
| 19 | + --muted: #6e7681; |
| 20 | + --sub: #8b949e; |
| 21 | + } |
| 22 | + |
| 23 | + html { scroll-behavior: smooth; } |
| 24 | + body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; font-size: 15px; line-height: 1.75; } |
| 25 | + code { font-family: 'JetBrains Mono', monospace; font-size: 12.5px; color: var(--accent); } |
| 26 | + a { color: var(--accent); text-decoration: none; } |
| 27 | + a:hover { text-decoration: underline; } |
| 28 | + |
| 29 | + .hero { padding: 72px 40px 56px; text-align: center; border-bottom: 1px solid var(--border); position: relative; overflow: hidden; } |
| 30 | + .hero::after { content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 600px; height: 1px; background: linear-gradient(90deg, transparent, var(--accent), transparent); } |
| 31 | + .hero-eyebrow { font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 3px; text-transform: uppercase; color: var(--accent); margin-bottom: 24px; } |
| 32 | + .hero h1 { font-size: clamp(30px, 5vw, 52px); font-weight: 700; letter-spacing: -1.5px; line-height: 1.1; margin-bottom: 10px; } |
| 33 | + .hero h1 span { color: var(--accent); } |
| 34 | + .hero h2 { font-size: 15px; font-weight: 400; color: var(--muted); margin-bottom: 48px; } |
| 35 | + |
| 36 | + .badges { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-bottom: 48px; } |
| 37 | + .badge { display: flex; border-radius: 4px; overflow: hidden; font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 500; } |
| 38 | + .badge-label { background: #161b22; border: 1px solid var(--border); border-right: none; padding: 5px 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; border-radius: 4px 0 0 4px; } |
| 39 | + .badge-value { padding: 5px 10px; font-weight: 600; color: #0d1117; border-radius: 0 4px 4px 0; } |
| 40 | + .badge.blue .badge-value { background: var(--accent); } |
| 41 | + .badge.navy .badge-value { background: #0077b6; } |
| 42 | + .badge.amber .badge-value { background: var(--amber); } |
| 43 | + .badge.gray .badge-value { background: var(--muted); color: var(--text); } |
| 44 | + |
| 45 | + .meta-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 0; } |
| 46 | + .meta-item { text-align: center; padding: 0 28px; } |
| 47 | + .meta-item + .meta-item { border-left: 1px solid var(--border); } |
| 48 | + .meta-item .label { font-size: 10px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; } |
| 49 | + .meta-item .value { font-size: 14px; font-weight: 500; color: var(--text); } |
| 50 | + |
| 51 | + .container { max-width: 800px; margin: 0 auto; padding: 64px 24px 80px; } |
| 52 | + .section { margin-bottom: 64px; } |
| 53 | + .section-header { display: flex; align-items: baseline; gap: 14px; margin-bottom: 32px; padding-bottom: 14px; border-bottom: 1px solid var(--border); } |
| 54 | + .section-number { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--accent); letter-spacing: 1px; } |
| 55 | + .section-header h2 { font-size: 18px; font-weight: 600; letter-spacing: -.3px; color: var(--text); } |
| 56 | + .section-header .tag { margin-left: auto; font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--amber); border: 1px solid var(--amber); border-radius: 4px; padding: 2px 8px; } |
| 57 | + |
| 58 | + .block { border-left: 2px solid var(--border); padding: 4px 0 4px 24px; margin-bottom: 32px; } |
| 59 | + .block-title { font-size: 11px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); margin-bottom: 14px; } |
| 60 | + |
| 61 | + .callout { border-left: 2px solid var(--accent); padding: 4px 0 4px 20px; margin-bottom: 32px; font-size: 14px; color: var(--sub); } |
| 62 | + .callout strong { color: var(--text); font-weight: 500; } |
| 63 | + |
| 64 | + .code-block { background: #0d1117; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; } |
| 65 | + .code-bar { display: flex; align-items: center; gap: 6px; padding: 9px 14px; border-bottom: 1px solid var(--border); } |
| 66 | + .dot { width: 11px; height: 11px; border-radius: 50%; } |
| 67 | + .dot.r { background: #ff5f57; } .dot.y { background: #ffbd2e; } .dot.g { background: #28c840; } |
| 68 | + .code-lang { margin-left: auto; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--muted); letter-spacing: 1px; text-transform: uppercase; } |
| 69 | + pre { padding: 18px 20px; overflow-x: auto; font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.8; color: #c9d1d9; white-space: pre; scrollbar-width: thin; scrollbar-color: #2d333b transparent; } |
| 70 | + .cm { color: var(--muted); } .cmd { color: #79c0ff; } .out { color: #c9d1d9; } .hsh { color: #f0883e; } .key { color: #ff7b72; } .str { color: #a5d6ff; } |
| 71 | + |
| 72 | + .reflection { border-left: 2px solid var(--accent); padding: 6px 0 6px 24px; font-size: 15px; line-height: 1.85; color: var(--sub); } |
| 73 | + .reflection strong { color: var(--text); font-weight: 500; } |
| 74 | + |
| 75 | + .checklist { display: flex; flex-direction: column; gap: 10px; } |
| 76 | + .checklist li { list-style: none; font-size: 13px; color: var(--sub); padding-left: 22px; position: relative; line-height: 1.6; } |
| 77 | + .checklist li::before { content: '○'; position: absolute; left: 0; color: var(--muted); } |
| 78 | + .checklist li strong { color: var(--text); font-weight: 500; } |
| 79 | + |
| 80 | + .journal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; } |
| 81 | + @media (max-width: 580px) { .journal-grid { grid-template-columns: 1fr; } } |
| 82 | + .journal-block { border-left: 2px solid var(--border); padding: 4px 0 4px 20px; } |
| 83 | + .journal-block h3 { font-size: 10px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); margin-bottom: 14px; } |
| 84 | + .journal-block ul { list-style: none; display: flex; flex-direction: column; gap: 8px; } |
| 85 | + .journal-block ul li { font-size: 13px; color: var(--sub); padding-left: 14px; position: relative; } |
| 86 | + .journal-block ul li::before { content: '–'; position: absolute; left: 0; color: var(--muted); } |
| 87 | + |
| 88 | + .placeholder { font-size: 13px; color: var(--muted); font-style: italic; } |
| 89 | + .todo { display: inline-block; font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--amber); border: 1px dashed var(--amber); border-radius: 4px; padding: 1px 7px; margin-right: 8px; } |
| 90 | + |
| 91 | + footer { border-top: 1px solid var(--border); padding: 28px 24px; text-align: center; font-size: 12px; color: var(--muted); letter-spacing: .3px; } |
| 92 | + footer strong { color: var(--sub); font-weight: 500; } |
| 93 | + </style> |
| 94 | +</head> |
| 95 | +<body> |
| 96 | + |
| 97 | +<!-- HERO --> |
| 98 | +<div class="hero"> |
| 99 | + <div class="hero-eyebrow">OpenLab Apprenticeship 2026</div> |
| 100 | + <h1>JOSA <span>OpenLab</span></h1> |
| 101 | + <h2>Week 05: Testing, CI/CD & GitHub Actions</h2> |
| 102 | + |
| 103 | + <div class="badges"> |
| 104 | + <div class="badge blue"> |
| 105 | + <span class="badge-label">Week</span> |
| 106 | + <span class="badge-value">05 of 11</span> |
| 107 | + </div> |
| 108 | + <div class="badge navy"> |
| 109 | + <span class="badge-label">Phase</span> |
| 110 | + <span class="badge-value">Foundations</span> |
| 111 | + </div> |
| 112 | + <div class="badge amber"> |
| 113 | + <span class="badge-label">Status</span> |
| 114 | + <span class="badge-value">In Progress</span> |
| 115 | + </div> |
| 116 | + </div> |
| 117 | + |
| 118 | + <div class="meta-row"> |
| 119 | + <div class="meta-item"> |
| 120 | + <div class="label">Apprentice</div> |
| 121 | + <div class="value">Qutibah Ananzeh</div> |
| 122 | + </div> |
| 123 | + <div class="meta-item"> |
| 124 | + <div class="label">Cohort</div> |
| 125 | + <div class="value">OpenLab 2026</div> |
| 126 | + </div> |
| 127 | + <div class="meta-item"> |
| 128 | + <div class="label">Deadline</div> |
| 129 | + <div class="value">Jun 13, 2026</div> |
| 130 | + </div> |
| 131 | + </div> |
| 132 | +</div> |
| 133 | + |
| 134 | +<!-- MAIN --> |
| 135 | +<div class="container"> |
| 136 | + |
| 137 | + <!-- THE STANDARD --> |
| 138 | + <div class="section"> |
| 139 | + <div class="section-header"> |
| 140 | + <span class="section-number">// 00</span> |
| 141 | + <h2>The Standard</h2> |
| 142 | + </div> |
| 143 | + |
| 144 | + <div class="callout"> |
| 145 | + <strong>The unifying principle:</strong> write the <em>cheapest</em> test that meaningfully exercises the behavior. A pure function gets a unit test. A function that talks to a database gets an integration test against a real (test) database, not a mocked one. A user-facing flow gets one happy-path E2E test. |
| 146 | + </div> |
| 147 | + |
| 148 | + <div class="block"> |
| 149 | + <div class="block-title">Test Pyramid & Testing Trophy</div> |
| 150 | + <ul class="checklist"> |
| 151 | + <li><strong>Pyramid (Cohn).</strong> Many fast unit tests at the bottom, fewer integration tests, very few slow E2E tests on top. The geometry is a cost model: a unit test runs in milliseconds; an E2E test can take minutes.</li> |
| 152 | + <li><strong>Trophy (Dodds).</strong> For modern frontend, lean on integration tests for the highest confidence per dollar, on a base of static analysis (TypeScript, ESLint).</li> |
| 153 | + </ul> |
| 154 | + </div> |
| 155 | + |
| 156 | + <div class="block"> |
| 157 | + <div class="block-title">Anatomy of a Good Unit Test</div> |
| 158 | + <ul class="checklist"> |
| 159 | + <li><strong>Arrange / Act / Assert.</strong> One logical assertion per test, so a failure points at one thing.</li> |
| 160 | + <li><strong>Name the behavior, not the function.</strong> <code>test_parser_handles_trailing_comma</code>, not <code>test_parse</code>.</li> |
| 161 | + <li><strong>Deterministic.</strong> No unseeded randomness, no real <code>now()</code> (inject a clock), no real network (inject a transport).</li> |
| 162 | + <li><strong>Fakes > mocks.</strong> Mock external services you don't own (Stripe, SendGrid); don't mock your own database, run a real one in CI.</li> |
| 163 | + </ul> |
| 164 | + </div> |
| 165 | + |
| 166 | + <div class="block"> |
| 167 | + <div class="block-title">Suggested Repositories</div> |
| 168 | + <p>Any project with a red CI badge · <a href="https://github.com/sdras/awesome-actions" target="_blank">sdras/awesome-actions</a> · one of my own repos for the workflow task.</p> |
| 169 | + </div> |
| 170 | + </div> |
| 171 | + |
| 172 | + <!-- TASK 1 --> |
| 173 | + <div class="section"> |
| 174 | + <div class="section-header"> |
| 175 | + <span class="section-number">// 01</span> |
| 176 | + <h2>Add Tests Where There Are None</h2> |
| 177 | + <span class="tag">Pending</span> |
| 178 | + </div> |
| 179 | + |
| 180 | + <div class="callout"> |
| 181 | + <strong>Goal:</strong> Scout for untested code in a GitHub repo. Write a test file (or extend an existing one) with well-structured <strong>Arrange / Act / Assert</strong> tests, then submit a PR. |
| 182 | + </div> |
| 183 | + |
| 184 | + <div class="block"> |
| 185 | + <div class="block-title">Plan</div> |
| 186 | + <ul class="checklist"> |
| 187 | + <li><span class="todo">todo</span>Pick a repo with thin coverage and identify a pure-ish function or module that has no tests.</li> |
| 188 | + <li><span class="todo">todo</span>Write A/A/A tests covering the happy path plus at least one edge case.</li> |
| 189 | + <li><span class="todo">todo</span>Open a PR and link it here.</li> |
| 190 | + </ul> |
| 191 | + <p class="placeholder" style="margin-top:14px;">Reflection, commands, and PR link to be filled in once the work is done.</p> |
| 192 | + </div> |
| 193 | + </div> |
| 194 | + |
| 195 | + <!-- TASK 2 --> |
| 196 | + <div class="section"> |
| 197 | + <div class="section-header"> |
| 198 | + <span class="section-number">// 02</span> |
| 199 | + <h2>Build a Real CI Workflow</h2> |
| 200 | + <span class="tag">Pending</span> |
| 201 | + </div> |
| 202 | + |
| 203 | + <div class="callout"> |
| 204 | + <strong>Goal:</strong> Add a complete <code>.github/workflows/ci.yml</code> to one of my repos with lint, test, a matrix over 2+ versions, dependency caching, and a status badge in the README. |
| 205 | + </div> |
| 206 | + |
| 207 | + <div class="block"> |
| 208 | + <div class="block-title">Reference Shape</div> |
| 209 | + <div class="code-block"> |
| 210 | + <div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="code-lang">.github/workflows/ci.yml</span></div> |
| 211 | + <pre><span class="key">name</span>: CI |
| 212 | +<span class="key">on</span>: |
| 213 | + <span class="key">push</span>: { <span class="key">branches</span>: [main] } |
| 214 | + <span class="key">pull_request</span>: |
| 215 | +<span class="key">jobs</span>: |
| 216 | + <span class="key">test</span>: |
| 217 | + <span class="key">runs-on</span>: <span class="str">${{ matrix.os }}</span> |
| 218 | + <span class="key">strategy</span>: |
| 219 | + <span class="key">fail-fast</span>: false |
| 220 | + <span class="key">matrix</span>: |
| 221 | + <span class="key">os</span>: [ubuntu-latest, macos-latest] |
| 222 | + <span class="key">node</span>: [20, 22] |
| 223 | + <span class="key">steps</span>: |
| 224 | + - <span class="key">uses</span>: actions/checkout@v4 |
| 225 | + - <span class="key">uses</span>: actions/setup-node@v4 |
| 226 | + <span class="key">with</span>: { <span class="key">node-version</span>: <span class="str">${{ matrix.node }}</span>, <span class="key">cache</span>: npm } |
| 227 | + - <span class="key">run</span>: npm ci |
| 228 | + - <span class="key">run</span>: npm run lint |
| 229 | + - <span class="key">run</span>: npm test</pre> |
| 230 | + </div> |
| 231 | + <ul class="checklist" style="margin-top:16px;"> |
| 232 | + <li><span class="todo">todo</span>Pick the target repo and wire in lint + test scripts.</li> |
| 233 | + <li><span class="todo">todo</span>Add the matrix + <code>cache: npm</code>, then add the status badge to the README.</li> |
| 234 | + </ul> |
| 235 | + <p class="placeholder" style="margin-top:14px;">Final workflow, badge, and reflection to be filled in once it's green on a real repo.</p> |
| 236 | + </div> |
| 237 | + </div> |
| 238 | + |
| 239 | + <!-- TASK 3 --> |
| 240 | + <div class="section"> |
| 241 | + <div class="section-header"> |
| 242 | + <span class="section-number">// 03</span> |
| 243 | + <h2>Run It Locally with act</h2> |
| 244 | + <span class="tag">Pending</span> |
| 245 | + </div> |
| 246 | + |
| 247 | + <div class="callout"> |
| 248 | + <strong>Goal:</strong> Reproduce the workflow locally with <a href="https://github.com/nektos/act" target="_blank">act</a> before pushing, and document any differences from the hosted runners in the journal. |
| 249 | + </div> |
| 250 | + |
| 251 | + <div class="block"> |
| 252 | + <div class="block-title">Plan</div> |
| 253 | + <ul class="checklist"> |
| 254 | + <li><span class="todo">todo</span>Install <code>act</code> and run the <code>test</code> job locally (<code>act -j test</code>).</li> |
| 255 | + <li><span class="todo">todo</span>Note where local diverges from hosted (Windows runners unsupported, action quirks, secrets via <code>--secret-file</code>).</li> |
| 256 | + </ul> |
| 257 | + <p class="placeholder" style="margin-top:14px;">Local run output and the list of differences to be filled in.</p> |
| 258 | + </div> |
| 259 | + </div> |
| 260 | + |
| 261 | + <!-- SOFT SKILL --> |
| 262 | + <div class="section"> |
| 263 | + <div class="section-header"> |
| 264 | + <span class="section-number">// 04</span> |
| 265 | + <h2>Soft Skill, Time Management & Deep Work</h2> |
| 266 | + <span class="tag">Pending</span> |
| 267 | + </div> |
| 268 | + |
| 269 | + <div class="callout"> |
| 270 | + <strong>Discipline:</strong> testing and CI work reward uninterrupted focus. <strong>Time-box</strong> debugging sessions ("90 minutes, then re-evaluate"), and do <strong>one thing at a time</strong>, task-switching costs ~25 minutes to reload context. An OSS-relevant habit: batch code-review responses once or twice a day instead of replying to every comment as it lands. |
| 271 | + </div> |
| 272 | + |
| 273 | + <div class="block"> |
| 274 | + <div class="block-title">Reflection</div> |
| 275 | + <p class="placeholder">To be written at the end of the week.</p> |
| 276 | + </div> |
| 277 | + </div> |
| 278 | + |
| 279 | + <!-- JOURNAL --> |
| 280 | + <div class="section"> |
| 281 | + <div class="section-header"> |
| 282 | + <span class="section-number">// 05</span> |
| 283 | + <h2>Weekly Journal</h2> |
| 284 | + </div> |
| 285 | + |
| 286 | + <div class="journal-grid"> |
| 287 | + <div class="journal-block"> |
| 288 | + <h3>What I learned</h3> |
| 289 | + <ul><li class="placeholder">To be filled in.</li></ul> |
| 290 | + </div> |
| 291 | + <div class="journal-block"> |
| 292 | + <h3>What was hardest</h3> |
| 293 | + <ul><li class="placeholder">To be filled in.</li></ul> |
| 294 | + </div> |
| 295 | + <div class="journal-block"> |
| 296 | + <h3>What's blocking me</h3> |
| 297 | + <ul><li class="placeholder">To be filled in.</li></ul> |
| 298 | + </div> |
| 299 | + </div> |
| 300 | + </div> |
| 301 | + |
| 302 | +</div> |
| 303 | + |
| 304 | +<footer> |
| 305 | + Jordan Open Source Association · <strong>OpenLab 2026</strong> · Qutibah Ananzeh |
| 306 | +</footer> |
| 307 | + |
| 308 | +</body> |
| 309 | +</html> |
0 commit comments