From ca54bbd0cbad92221c96bb6956ae9066eee6bf27 Mon Sep 17 00:00:00 2001 From: Emilie McGregor Date: Tue, 28 Apr 2026 13:02:10 -0700 Subject: [PATCH 1/5] feat: add failure notifications to Python unit tests workflow --- .github/workflows/testsPython.yml | 118 ++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testsPython.yml b/.github/workflows/testsPython.yml index 452f71d..7d251c5 100644 --- a/.github/workflows/testsPython.yml +++ b/.github/workflows/testsPython.yml @@ -22,6 +22,11 @@ name: Python Unit Tests on: workflow_dispatch: # Allows the workflow to be manually triggered from the GitHub Actions tab + inputs: + notify_email: + description: 'Recipient email(s) for failure notification (comma-separated)' + required: false + default: '' pull_request: # paths: # Trigger workflow on pull requests, but - "**.py" # only if Python files are changed @@ -64,16 +69,117 @@ jobs: # Job #2: Notifications (Mini-capstone assignment) # This job will run after the Python unit tests and - # is scaffolded to facilitate sending notifications based - # on the test results. + # sends notifications when tests fail via Slack, email, and GitHub Issues. + # if: always() is required — without it GitHub skips this job when the + # upstream job fails, which defeats the purpose. notifications: needs: python-unit-tests runs-on: ubuntu-latest + if: always() + permissions: + issues: write steps: - - name: Notify on test results + - name: Write job summary + if: always() run: | - if [ "${{ needs.python-unit-tests.result }}" == "success" ]; then - echo "success notifications go here" + if [ "${{ needs.python-unit-tests.result }}" == "failure" ]; then + echo "## ❌ Python Tests Failed" >> $GITHUB_STEP_SUMMARY else - echo "failure notifications go here" + echo "## ✅ Python Tests Passed" >> $GITHUB_STEP_SUMMARY fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Actor | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY + echo "| Run | [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) |" >> $GITHUB_STEP_SUMMARY + + - name: Notify Slack on failure + if: needs.python-unit-tests.result == 'failure' && secrets.SLACK_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@v2 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": ":x: *Python tests failed* on `${{ github.ref_name }}`", + "attachments": [{ + "color": "danger", + "fields": [ + { "title": "Repo", "value": "${{ github.repository }}", "short": true }, + { "title": "Triggered by", "value": "${{ github.actor }}", "short": true }, + { "title": "Commit", "value": "${{ github.sha }}" }, + { "title": "Run", "value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } + ] + }] + } + + - name: Send failure email + if: needs.python-unit-tests.result == 'failure' && secrets.MAIL_USERNAME != '' + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 465 + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + to: ${{ github.event.inputs.notify_email || secrets.NOTIFY_EMAIL }} + from: GitHub Actions <${{ secrets.MAIL_USERNAME }}> + subject: "❌ Tests failed — ${{ github.repository }} (${{ github.ref_name }})" + body: | + Python unit tests failed. + + Repository : ${{ github.repository }} + Branch : ${{ github.ref_name }} + Commit : ${{ github.sha }} + Triggered by: ${{ github.actor }} + Run URL : ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + - name: Create GitHub issue on failure + if: needs.python-unit-tests.result == 'failure' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `❌ Tests failed on ${context.ref} (${context.sha.substring(0, 7)})`, + body: [ + `**Python unit tests failed.**`, + ``, + `| Field | Value |`, + `|---|---|`, + `| Branch | \`${context.ref}\` |`, + `| Commit | \`${context.sha}\` |`, + `| Triggered by | ${context.actor} |`, + `| Run | [View logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) |`, + ].join('\n'), + labels: ['bug'] + }); + + - name: Close GitHub issue on success + if: needs.python-unit-tests.result == 'success' + uses: actions/github-script@v7 + with: + script: | + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'bug' + }); + const ciIssues = issues.data.filter(i => i.title.startsWith('❌ Tests failed')); + for (const issue of ciIssues) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `✅ Tests are passing again as of commit \`${context.sha.substring(0, 7)}\` on \`${context.ref}\`. Closing.` + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + } From 42f69c25b1cfcdcc10a97b5183a6586b1b065f1b Mon Sep 17 00:00:00 2001 From: Emilie McGregor Date: Tue, 28 Apr 2026 13:08:42 -0700 Subject: [PATCH 2/5] fix: use env vars instead of secrets context in step conditions --- .github/workflows/testsPython.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testsPython.yml b/.github/workflows/testsPython.yml index 7d251c5..86aee80 100644 --- a/.github/workflows/testsPython.yml +++ b/.github/workflows/testsPython.yml @@ -78,6 +78,9 @@ jobs: if: always() permissions: issues: write + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} steps: - name: Write job summary if: always() @@ -96,7 +99,7 @@ jobs: echo "| Run | [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) |" >> $GITHUB_STEP_SUMMARY - name: Notify Slack on failure - if: needs.python-unit-tests.result == 'failure' && secrets.SLACK_WEBHOOK_URL != '' + if: needs.python-unit-tests.result == 'failure' && env.SLACK_WEBHOOK_URL != '' uses: slackapi/slack-github-action@v2 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} @@ -116,7 +119,7 @@ jobs: } - name: Send failure email - if: needs.python-unit-tests.result == 'failure' && secrets.MAIL_USERNAME != '' + if: needs.python-unit-tests.result == 'failure' && env.MAIL_USERNAME != '' uses: dawidd6/action-send-mail@v3 with: server_address: smtp.gmail.com From d3352dc9d3f4bff7c56b3e717d8f313b6ff20b39 Mon Sep 17 00:00:00 2001 From: Emilie McGregor Date: Tue, 28 Apr 2026 13:46:53 -0700 Subject: [PATCH 3/5] fix: handle empty string env vars for numeric settings --- app/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/settings.py b/app/settings.py index 3f9b71f..8ffb421 100644 --- a/app/settings.py +++ b/app/settings.py @@ -27,7 +27,7 @@ # MySQL database settings MYSQL_HOST = os.getenv("MYSQL_HOST", SET_ME_PLEASE) -MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) +MYSQL_PORT = int(os.getenv("MYSQL_PORT") or "3306") MYSQL_USER = os.getenv("MYSQL_USER", SET_ME_PLEASE) MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", SET_ME_PLEASE) MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", SET_ME_PLEASE) From 7845fdcdb32f66359192f38581d830e0f6bc22da Mon Sep 17 00:00:00 2001 From: Emilie McGregor Date: Tue, 28 Apr 2026 14:30:37 -0700 Subject: [PATCH 4/5] chore: add mise.toml for Python environment and task management --- mise.toml | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 mise.toml diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..fb568ad --- /dev/null +++ b/mise.toml @@ -0,0 +1,63 @@ +[tools] +python = "3.13" + +[env] +_.python.venv = { path = ".venv", create = true } + +[tasks.init] +description = "Install production dependencies" +run = [ + "pip install --upgrade pip", + "pip install -r requirements/base.txt", +] + +[tasks.init-dev] +description = "Install all dependencies including dev tools" +depends = ["init"] +run = [ + "npm install", + "pip install -r requirements/local.txt", + "pre-commit install", +] + +[tasks.test] +description = "Run Python unit tests" +run = "python -m unittest discover -s app/" + +[tasks.coverage] +description = "Run tests with coverage report" +run = [ + "python -m coverage run --source=app --omit='app/tests/*' -m unittest discover -s app/tests", + "python -m coverage report -m --omit='app/tests/*'", + "python -m coverage xml --omit='app/tests/*'", +] + +[tasks.lint] +description = "Run all linters" +run = [ + "isort .", + "pre-commit run --all-files", + "black .", + "flake8 ./app/", + "pylint ./app/**/*.py", +] + +[tasks.pre-commit] +description = "Install and run pre-commit hooks" +run = [ + "pre-commit install", + "pre-commit autoupdate", + "pre-commit run --all-files", +] + +[tasks.tear-down] +description = "Remove mise-managed virtual environment and build artifacts" +run = "rm -rf .venv node_modules app/__pycache__ package-lock.json" + +[tasks.analyze] +description = "Generate code analysis report" +run = "cloc . --exclude-ext=svg,json,zip --vcs=git" + +[tasks.release] +description = "Force a new GitHub release" +run = "git commit -m 'fix: force a new release' --allow-empty && git push" From f887ff3ed598be734a6c1069ff2052da5c603d0a Mon Sep 17 00:00:00 2001 From: Emilie McGregor Date: Tue, 28 Apr 2026 14:30:37 -0700 Subject: [PATCH 5/5] docs: add CI notifications testing guide --- doc/CI_NOTIFICATIONS_TESTING_GUIDE.md | 294 ++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 doc/CI_NOTIFICATIONS_TESTING_GUIDE.md diff --git a/doc/CI_NOTIFICATIONS_TESTING_GUIDE.md b/doc/CI_NOTIFICATIONS_TESTING_GUIDE.md new file mode 100644 index 0000000..63ae4c4 --- /dev/null +++ b/doc/CI_NOTIFICATIONS_TESTING_GUIDE.md @@ -0,0 +1,294 @@ +# CI Notifications — Complete Testing Guide + +## Overview & Total Time + +**~60–75 minutes total.** Three natural stopping points are marked clearly. + +--- + +## Part 1 — Gmail App Password + +**⏱ ~10 min** + +You need an App Password (not your regular Gmail password) because Google blocks direct +password auth for automated tools. + +1. Go to [myaccount.google.com](https://myaccount.google.com) → **Security** +2. Ensure **2-Step Verification** is turned on (required for App Passwords) +3. Search for **"App Passwords"** in the search bar at the top +4. Click **App Passwords** → under "Select app" choose **Mail**, under "Select device" choose **Other** → type `GitHub Actions` +5. Click **Generate** — copy the 16-character password shown (spaces don't matter) +6. Save it somewhere safe — Google won't show it again + +--- + +## Part 2 — Free Slack Workspace + Webhook + +**⏱ ~15 min** + +1. Go to **slack.com** → click **"Get started for free"** +2. Sign up with your email → name the workspace anything (e.g. `CI Test`) +3. Skip inviting teammates +4. Once inside your workspace, go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From scratch** +5. Name it `GitHub Actions`, select your workspace → **Create App** +6. On the left sidebar → **Incoming Webhooks** → toggle **Activate Incoming Webhooks** to On +7. Scroll down → **Add New Webhook to Workspace** → select any channel (e.g. `#general`) → **Allow** +8. Copy the **Webhook URL** (starts with `https://hooks.slack.com/services/...`) + +--- + +## Part 3 — Add GitHub Secrets + +**⏱ ~10 min** + +In your forked repo on GitHub: **Settings → Secrets and variables → Actions → New repository secret** + +Add all of these secrets: + +### Notification secrets + +| Secret name | Value | +|---|---| +| `SLACK_WEBHOOK_URL` | The URL from Part 2 | +| `MAIL_USERNAME` | Your Gmail address (e.g. `you@gmail.com`) | +| `MAIL_PASSWORD` | The 16-char app password from Part 1 | +| `NOTIFY_EMAIL` | Where to receive emails — can be same as `MAIL_USERNAME` | + +### OpenAI secrets + +| Secret name | Value | +|------------------|--------------------------------------------| +| `OPENAI_API_KEY` | Your OpenAI API key (starts with `sk-`) | + +> Find it at **platform.openai.com → API keys**. `OPENAI_API_ORGANIZATION` is **not needed** — it's passed by the workflow but not used by the code. + +### MySQL secrets (dummy values are fine — tests mock the database) + +| Secret name | Value | +|---|---| +| `MYSQL_HOST` | `localhost` | +| `MYSQL_USER` | `test_user` | +| `MYSQL_PASSWORD` | `password` | +| `MYSQL_DATABASE` | `test` | + +> `MYSQL_PORT` does **not** need to be set — the custom action defaults it to `3306`. + +### Codecov (optional) + +The workflow uploads coverage reports to Codecov. You have two options: + +**Option A — Skip it (quickest):** The custom action at `.github/actions/tests/python/action.yml` +has `fail_ci_if_error: true` on the Codecov upload step. Change it to `false` and the upload +will fail silently without blocking the workflow. No account needed. + +**Option B — Set it up free:** Go to **codecov.io** → Sign in with GitHub → add your repo → +copy the token → add it as `CODECOV_TOKEN`. Codecov is a legitimate service (owned by Sentry) +used by major open source projects. Free for public repos. + +--- + +## 🛑 STOP 1 — Good place to pause (~35 min in) + +Everything is configured. You have all credentials in place. Pick this back up at Part 4 when ready. + +--- + +## Part 4 — Create a Branch and Push Your Changes + +**⏱ ~5 min** + +In your terminal, from the project root: + +```bash +git checkout -b feature/ci-notifications +git add .github/workflows/testsPython.yml +git commit -m "feat: add failure notifications to Python unit tests workflow" +git push -u origin feature/ci-notifications +``` + +Your changes are now on GitHub on a feature branch. The workflow will **not** trigger yet — +the `push` trigger only watches `main` and `next`. That's fine — we'll trigger it manually next. + +--- + +## Part 5 — Test 1: Verify the Happy Path (Tests Pass) + +**⏱ ~5 min** + +This confirms the workflow runs cleanly before introducing any failures. + +1. Go to your repo on GitHub → **Actions** tab +2. Click **Python Unit Tests** in the left sidebar +3. Click **Run workflow** (top right) → select branch `feature/ci-notifications` → click **Run workflow** +4. Wait ~2–3 minutes for it to complete + +**What to verify:** + +- Both jobs (`python-unit-tests` and `notifications`) show green ✅ +- Click the `notifications` job → click **Write job summary** step → you should see a `✅ Python Tests Passed` table +- No Slack message, no email, no GitHub issue created (correct — only fires on failure) +- Click the **Summary** tab on the run — you'll see the markdown summary rendered there + +--- + +## 🛑 STOP 2 — Good place to pause (~40 min in) + +Basic workflow confirmed working. All setup validated. Pick up at Part 6 to test failure notifications. + +--- + +## Part 6 — Test 2: Introduce a Bug to Trigger Failure Notifications + +**⏱ ~10 min** + +Open `app/tests/test_utils.py` and make this one-line change on line 22: + +```python +# Before (original): +self.assertIn("\033[", result) + +# After (intentional break): +self.assertIn("THIS_WILL_NEVER_MATCH", result) +``` + +This test has no external dependencies (no database, no OpenAI key) so it will fail +cleanly and predictably — ideal for controlled testing. + +Then commit and push: + +```bash +git add app/tests/test_utils.py +git commit -m "test: intentional break to test CI notifications" +git push +``` + +Trigger the workflow manually (**Actions → Python Unit Tests → Run workflow → your branch**). + +**What to verify:** + +- `python-unit-tests` job goes red ❌ +- `notifications` job still runs (proof the `if: always()` fix works) +- Click **Write job summary** → see `❌ Python Tests Failed` table +- Check your Slack channel — message should arrive within ~30 seconds of the job finishing +- Check your email inbox — may take 1–2 minutes +- Go to **Issues** tab in your repo — a new issue titled `❌ Tests failed on refs/heads/feature/ci-notifications (...)` should appear with a `bug` label + +--- + +## Part 7 — Test 3: Fix the Bug and Verify Auto-Close + +**⏱ ~5 min** + +Use `git revert` to undo the break commit — no manual file editing needed: + +```bash +git revert HEAD --no-edit +git push +``` + +This creates a new commit that undoes the previous one, restoring the test to its original state. + +Trigger the workflow manually again. + +**What to verify:** + +- Both jobs green ✅ +- Go to **Issues** tab — the issue from Part 6 should now be **closed** +- Click into it — you should see a comment: `✅ Tests are passing again as of commit ... Closing.` + +--- + +## Part 8 — Test 4: Parameterized Email Recipient + +**⏱ ~5 min** + +This tests the `workflow_dispatch` input — the "optionally parameterize the addressee" +part of the assignment requirement. + +Re-apply the break by reverting the revert: + +```bash +git revert HEAD --no-edit +git push +``` + +Then: + +1. **Actions → Python Unit Tests → Run workflow** +2. You'll see an input field: **"Recipient email(s) for failure notification"** +3. Enter a different address (a teammate, another account, or comma-separated list) +4. Verify the email arrives at the address you typed, not `NOTIFY_EMAIL` + +Revert again to restore passing tests and confirm issue auto-closes: + +```bash +git revert HEAD --no-edit +git push +``` + +--- + +## Part 9 — Test 5: Verify Graceful Degradation (No Slack Secret) + +**⏱ ~5 min** + +This validates that the workflow doesn't break for contributors who haven't configured Slack — +the guard `secrets.SLACK_WEBHOOK_URL != ''` should cause the step to be silently skipped. + +- **Step 1:** Go to **Settings → Secrets and variables → Actions** → find `SLACK_WEBHOOK_URL` → delete it +- **Step 2:** Re-apply the break: + +```bash +git revert HEAD --no-edit +git push +``` + +- **Step 3:** Trigger the workflow manually + +**What to verify:** + +- `notifications` job completes successfully ✅ (not failed) +- The **Notify Slack on failure** step shows as **skipped** (greyed out in the Actions UI) +- Email notification still fires normally +- GitHub issue still gets created normally +- Job summary still shows ❌ + +- **Step 4:** Revert to fix the tests and confirm issue auto-closes: + +```bash +git revert HEAD --no-edit +git push +``` + +- **Step 5:** Re-add the `SLACK_WEBHOOK_URL` secret if you want Slack notifications restored + +--- + +## 🛑 STOP 3 — Everything fully exercised (~80 min in) + +All features validated end-to-end. You're done. + +--- + +## Summary of What You've Built + +| Feature | Trigger | Where to see it | +|---|---|---| +| Job summary | Every run | Actions → run → Summary tab | +| Slack notification | Test failure | Your Slack channel | +| Email notification | Test failure | Inbox (or typed address on manual run) | +| GitHub issue | Test failure | Repo Issues tab | +| Auto-close issue | Tests pass after failure | Closed issue with comment | + +--- + +## Pre-Flight Checklist + +Before starting, confirm all of these: + +- [ ] Repo is **your fork** (not the upstream FullStackWithLawrence repo) — secrets and Actions only exist on your fork +- [ ] Branch is pushed to **your fork**, not upstream +- [ ] All 4 secrets are added under your fork's Settings +- [ ] `bug` label exists in your repo's Issues → Labels (it's a GitHub default, should already be there) +- [ ] Gmail 2-Step Verification is on (required for App Passwords to work) +- [ ] Slack workspace is created and webhook URL is copied before adding the secret