diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml old mode 100755 new mode 100644 index 3236dda..ca64d6a --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -20,3 +20,7 @@ jobs: - name: Build site run: make + + - name: Monitor build and notify Discord + if: always() + run: /snap/bin/pwsh -NoProfile -File ./gitea-build-monitor.ps1 diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/.packages/archives/elpa/archive-contents b/.packages/archives/elpa/archive-contents old mode 100755 new mode 100644 diff --git a/.packages/archives/elpa/archive-contents.signed b/.packages/archives/elpa/archive-contents.signed old mode 100755 new mode 100644 diff --git a/.packages/archives/melpa/archive-contents b/.packages/archives/melpa/archive-contents old mode 100755 new mode 100644 diff --git a/.packages/htmlize-20250724.1703/htmlize-autoloads.el b/.packages/htmlize-20250724.1703/htmlize-autoloads.el old mode 100755 new mode 100644 diff --git a/.packages/htmlize-20250724.1703/htmlize-pkg.el b/.packages/htmlize-20250724.1703/htmlize-pkg.el old mode 100755 new mode 100644 diff --git a/.packages/htmlize-20250724.1703/htmlize.el b/.packages/htmlize-20250724.1703/htmlize.el old mode 100755 new mode 100644 diff --git a/.packages/htmlize-20250724.1703/htmlize.elc b/.packages/htmlize-20250724.1703/htmlize.elc old mode 100755 new mode 100644 diff --git a/.projectile b/.projectile old mode 100755 new mode 100644 diff --git a/.stignore b/.stignore old mode 100755 new mode 100644 diff --git a/Makefile b/Makefile old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/assets/New Document.rnote b/assets/New Document.rnote old mode 100755 new mode 100644 diff --git a/assets/emacs-calibre.rnote b/assets/emacs-calibre.rnote old mode 100755 new mode 100644 diff --git a/assets/fonts/NotoSans-Regular.ttf b/assets/fonts/NotoSans-Regular.ttf old mode 100755 new mode 100644 diff --git a/assets/fonts/Roboto-Regular.ttf b/assets/fonts/Roboto-Regular.ttf old mode 100755 new mode 100644 diff --git a/assets/fonts/times.ttf b/assets/fonts/times.ttf old mode 100755 new mode 100644 diff --git a/assets/icons/apple-touch-icon.png b/assets/icons/apple-touch-icon.png old mode 100755 new mode 100644 diff --git a/assets/icons/icons8-favicon-50.png b/assets/icons/icons8-favicon-50.png old mode 100755 new mode 100644 diff --git a/assets/icons/icons8-film-tape-100.png b/assets/icons/icons8-film-tape-100.png old mode 100755 new mode 100644 diff --git a/assets/icons/icons8-z-key-64.png b/assets/icons/icons8-z-key-64.png old mode 100755 new mode 100644 diff --git a/assets/images/1_to_2.svg b/assets/images/1_to_2.svg old mode 100755 new mode 100644 diff --git a/assets/images/240226-pr.png b/assets/images/240226-pr.png old mode 100755 new mode 100644 diff --git a/assets/images/26-02-26-di-errors-2.png b/assets/images/26-02-26-di-errors-2.png old mode 100755 new mode 100644 diff --git a/assets/images/26-02-26-di-errors.png b/assets/images/26-02-26-di-errors.png old mode 100755 new mode 100644 diff --git a/assets/images/27-02-26-xserver.png b/assets/images/27-02-26-xserver.png old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-03-12-journeys-branch.png b/assets/images/blogs/2026-03-12-journeys-branch.png old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-03-18-slack-dag-manager.png b/assets/images/blogs/2026-03-18-slack-dag-manager.png old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-03-23-install-emacs.png b/assets/images/blogs/2026-03-23-install-emacs.png old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-03-24-lunch.jpg b/assets/images/blogs/2026-03-24-lunch.jpg old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-03-29-dinner-1.jpg b/assets/images/blogs/2026-03-29-dinner-1.jpg old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-03-29-dinner-2.jpg b/assets/images/blogs/2026-03-29-dinner-2.jpg old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-03-29-dinner-3.jpg b/assets/images/blogs/2026-03-29-dinner-3.jpg old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-04-02-slack.png b/assets/images/blogs/2026-04-02-slack.png old mode 100755 new mode 100644 diff --git a/assets/images/blogs/2026-05-01-sun.jpg b/assets/images/blogs/2026-05-01-sun.jpg old mode 100755 new mode 100644 diff --git a/assets/images/career/2025-10-10-stress-in-workplace.png b/assets/images/career/2025-10-10-stress-in-workplace.png old mode 100755 new mode 100644 diff --git a/assets/images/career/2025-10-10-user_stories.jpg b/assets/images/career/2025-10-10-user_stories.jpg old mode 100755 new mode 100644 diff --git a/assets/images/career/2025-11-05-retrospective_boat_model.png b/assets/images/career/2025-11-05-retrospective_boat_model.png old mode 100755 new mode 100644 diff --git a/assets/images/career/2025-11-10-Aware-of-the-Benefits-of-Normalisation.png b/assets/images/career/2025-11-10-Aware-of-the-Benefits-of-Normalisation.png old mode 100755 new mode 100644 diff --git a/assets/images/career/2025-11-15-airflow.png b/assets/images/career/2025-11-15-airflow.png old mode 100755 new mode 100644 diff --git a/assets/images/career/2025-12-02-retros.jpg b/assets/images/career/2025-12-02-retros.jpg old mode 100755 new mode 100644 diff --git a/assets/images/career/2026-01-17-airflow-flow.png b/assets/images/career/2026-01-17-airflow-flow.png old mode 100755 new mode 100644 diff --git a/assets/images/career/2026-01-17-datamart-high-level.png b/assets/images/career/2026-01-17-datamart-high-level.png old mode 100755 new mode 100644 diff --git a/assets/images/career/2026-01-17-high-level-dora.png b/assets/images/career/2026-01-17-high-level-dora.png old mode 100755 new mode 100644 diff --git a/assets/images/career/2026-03-05-api-protocols.png b/assets/images/career/2026-03-05-api-protocols.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134403.png b/assets/images/career/owasp/Screenshot 2025-10-16 134403.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134438.png b/assets/images/career/owasp/Screenshot 2025-10-16 134438.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134457.png b/assets/images/career/owasp/Screenshot 2025-10-16 134457.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134512.png b/assets/images/career/owasp/Screenshot 2025-10-16 134512.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/swappy-20251106-214613.png b/assets/images/career/owasp/swappy-20251106-214613.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/swappy-20251106-214648.png b/assets/images/career/owasp/swappy-20251106-214648.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/swappy-20251106-214700.png b/assets/images/career/owasp/swappy-20251106-214700.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/swappy-20251106-214709.png b/assets/images/career/owasp/swappy-20251106-214709.png old mode 100755 new mode 100644 diff --git a/assets/images/career/owasp/swappy-20251106-214717.png b/assets/images/career/owasp/swappy-20251106-214717.png old mode 100755 new mode 100644 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 112139.png b/assets/images/career/wireframes/Screenshot 2025-10-31 112139.png old mode 100755 new mode 100644 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 112206.png b/assets/images/career/wireframes/Screenshot 2025-10-31 112206.png old mode 100755 new mode 100644 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 112427.png b/assets/images/career/wireframes/Screenshot 2025-10-31 112427.png old mode 100755 new mode 100644 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 115621.png b/assets/images/career/wireframes/Screenshot 2025-10-31 115621.png old mode 100755 new mode 100644 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 115949.png b/assets/images/career/wireframes/Screenshot 2025-10-31 115949.png old mode 100755 new mode 100644 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 120038.png b/assets/images/career/wireframes/Screenshot 2025-10-31 120038.png old mode 100755 new mode 100644 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 120203.png b/assets/images/career/wireframes/Screenshot 2025-10-31 120203.png old mode 100755 new mode 100644 diff --git a/assets/images/gr.png b/assets/images/gr.png old mode 100755 new mode 100644 diff --git a/assets/images/hzone/patpat.png b/assets/images/hzone/patpat.png old mode 100755 new mode 100644 diff --git a/assets/images/lima-third-meeting.jpg b/assets/images/lima-third-meeting.jpg old mode 100755 new mode 100644 diff --git a/assets/images/org-roam-ui-graph.png b/assets/images/org-roam-ui-graph.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/02-12-25-retros.jpg b/assets/images/reviews/02-12-25-retros.jpg old mode 100755 new mode 100644 diff --git a/assets/images/reviews/07-12-25-daily-retros.jpg b/assets/images/reviews/07-12-25-daily-retros.jpg old mode 100755 new mode 100644 diff --git a/assets/images/reviews/15-11-2025-guac.png b/assets/images/reviews/15-11-2025-guac.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/16-11-25-daily-retros.jpg b/assets/images/reviews/16-11-25-daily-retros.jpg old mode 100755 new mode 100644 diff --git a/assets/images/reviews/23-11-2025-netdata.png b/assets/images/reviews/23-11-2025-netdata.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/23-11-2025-reader.png b/assets/images/reviews/23-11-2025-reader.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/23-11-2025-vnc.png b/assets/images/reviews/23-11-2025-vnc.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/23-11-25-daily-retros.jpg b/assets/images/reviews/23-11-25-daily-retros.jpg old mode 100755 new mode 100644 diff --git a/assets/images/reviews/30-11-2025-acutal.png b/assets/images/reviews/30-11-2025-acutal.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/30-11-25-daily-retros.jpg b/assets/images/reviews/30-11-25-daily-retros.jpg old mode 100755 new mode 100644 diff --git a/assets/images/reviews/PDT-bruno-vehicle-web-api.png b/assets/images/reviews/PDT-bruno-vehicle-web-api.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/c-sharp-vehicle-breakpoints.png b/assets/images/reviews/c-sharp-vehicle-breakpoints.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/competencies/competencies-09-11-25.png b/assets/images/reviews/competencies/competencies-09-11-25.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/competencies/dev-plan-09-11-25.png b/assets/images/reviews/competencies/dev-plan-09-11-25.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/competency-powerbi-report.png b/assets/images/reviews/competency-powerbi-report.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/journeys-api-side-by-side-upgrade.png b/assets/images/reviews/journeys-api-side-by-side-upgrade.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-1.png b/assets/images/reviews/probation-objectives/probation-objectives-1.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-2.png b/assets/images/reviews/probation-objectives/probation-objectives-2.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-3.png b/assets/images/reviews/probation-objectives/probation-objectives-3.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-4.png b/assets/images/reviews/probation-objectives/probation-objectives-4.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-5.png b/assets/images/reviews/probation-objectives/probation-objectives-5.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/probation-objectives/rotation-calendar.png b/assets/images/reviews/probation-objectives/rotation-calendar.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-03-15-timesheet.png b/assets/images/reviews/timesheets/2026-03-15-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-03-22-timesheet.png b/assets/images/reviews/timesheets/2026-03-22-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-03-29-timesheet.png b/assets/images/reviews/timesheets/2026-03-29-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-04-05-timesheet.png b/assets/images/reviews/timesheets/2026-04-05-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-04-12-timesheet.png b/assets/images/reviews/timesheets/2026-04-12-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-04-19-timesheet.png b/assets/images/reviews/timesheets/2026-04-19-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-04-26-timesheet.png b/assets/images/reviews/timesheets/2026-04-26-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/2026-05-03-timesheet.png b/assets/images/reviews/timesheets/2026-05-03-timesheet.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/timesheet-01-02.png b/assets/images/reviews/timesheets/timesheet-01-02.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/timesheet-01-03-26.png b/assets/images/reviews/timesheets/timesheet-01-03-26.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/timesheet-08-02.png b/assets/images/reviews/timesheets/timesheet-08-02.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/timesheet-08-03-26.png b/assets/images/reviews/timesheets/timesheet-08-03-26.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/timesheet-15-02-26.png b/assets/images/reviews/timesheets/timesheet-15-02-26.png old mode 100755 new mode 100644 diff --git a/assets/images/reviews/timesheets/timesheet-22-02-26.png b/assets/images/reviews/timesheets/timesheet-22-02-26.png old mode 100755 new mode 100644 diff --git a/assets/images/syntax/assets.png b/assets/images/syntax/assets.png old mode 100755 new mode 100644 diff --git a/assets/images/syntax/filebrowser.png b/assets/images/syntax/filebrowser.png old mode 100755 new mode 100644 diff --git a/assets/images/syntax/not_clutter.png b/assets/images/syntax/not_clutter.png old mode 100755 new mode 100644 diff --git a/assets/rnote/07-12-2025-retrospectives.rnote b/assets/rnote/07-12-2025-retrospectives.rnote old mode 100755 new mode 100644 diff --git a/assets/rnote/09-11-2025-nginx-architecture.rnote b/assets/rnote/09-11-2025-nginx-architecture.rnote old mode 100755 new mode 100644 diff --git a/assets/rnote/09-11-2025-retrospectives.rnote b/assets/rnote/09-11-2025-retrospectives.rnote old mode 100755 new mode 100644 diff --git a/assets/rnote/10-08-2025-weekly-review.rnote b/assets/rnote/10-08-2025-weekly-review.rnote old mode 100755 new mode 100644 diff --git a/assets/rnote/2025-08-08-emacs.rnote b/assets/rnote/2025-08-08-emacs.rnote old mode 100755 new mode 100644 diff --git a/assets/rnote/23-11-2025-retrospectives.rnote b/assets/rnote/23-11-2025-retrospectives.rnote old mode 100755 new mode 100644 diff --git a/assets/rnote/30-11-2025-retrospectives.rnote b/assets/rnote/30-11-2025-retrospectives.rnote old mode 100755 new mode 100644 diff --git a/assets/rnote/retros-template.rnote b/assets/rnote/retros-template.rnote old mode 100755 new mode 100644 diff --git a/assets/scripts/bigger-picture.min.js b/assets/scripts/bigger-picture.min.js old mode 100755 new mode 100644 diff --git a/assets/scripts/comments.js b/assets/scripts/comments.js old mode 100755 new mode 100644 diff --git a/assets/scripts/competency-status-board.js b/assets/scripts/competency-status-board.js old mode 100755 new mode 100644 diff --git a/assets/scripts/gallery-init.js b/assets/scripts/gallery-init.js old mode 100755 new mode 100644 diff --git a/assets/scripts/lunr.js b/assets/scripts/lunr.js old mode 100755 new mode 100644 diff --git a/assets/scripts/mermaid.min.js b/assets/scripts/mermaid.min.js old mode 100755 new mode 100644 diff --git a/assets/scripts/notes.js b/assets/scripts/notes.js old mode 100755 new mode 100644 diff --git a/assets/scripts/script.js b/assets/scripts/script.js old mode 100755 new mode 100644 diff --git a/assets/scripts/search.js b/assets/scripts/search.js old mode 100755 new mode 100644 diff --git a/assets/scripts/sitemap-interactive.js b/assets/scripts/sitemap-interactive.js old mode 100755 new mode 100644 diff --git a/assets/scripts/svg-pan-zoom.min.js b/assets/scripts/svg-pan-zoom.min.js old mode 100755 new mode 100644 diff --git a/assets/scripts/wird-tracker.js b/assets/scripts/wird-tracker.js old mode 100755 new mode 100644 diff --git a/assets/scripts/zhd.js b/assets/scripts/zhd.js old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/07-12-2025-retrospectives.svg b/assets/sketchnotes/07-12-2025-retrospectives.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/09-11-2025-nginx-architecture.svg b/assets/sketchnotes/09-11-2025-nginx-architecture.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/09-11-2025-retrospectives.svg b/assets/sketchnotes/09-11-2025-retrospectives.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/10-08-2025-weekly-review.svg b/assets/sketchnotes/10-08-2025-weekly-review.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/16-11-2025-retrospectives.svg b/assets/sketchnotes/16-11-2025-retrospectives.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/18-01-26-retro.png b/assets/sketchnotes/18-01-26-retro.png old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/2025-08-08-emacs.svg b/assets/sketchnotes/2025-08-08-emacs.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/23-11-2025-retrospectives.svg b/assets/sketchnotes/23-11-2025-retrospectives.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/25-01-26-retro.png b/assets/sketchnotes/25-01-26-retro.png old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/30-11-2025-retrospectives.svg b/assets/sketchnotes/30-11-2025-retrospectives.svg old mode 100755 new mode 100644 diff --git a/assets/sketchnotes/elt-yed.svg b/assets/sketchnotes/elt-yed.svg old mode 100755 new mode 100644 diff --git a/assets/styles/bigger-picture.min.css b/assets/styles/bigger-picture.min.css old mode 100755 new mode 100644 diff --git a/assets/styles/comments.css b/assets/styles/comments.css old mode 100755 new mode 100644 diff --git a/assets/styles/kanban.css b/assets/styles/kanban.css old mode 100755 new mode 100644 diff --git a/assets/styles/media.css b/assets/styles/media.css old mode 100755 new mode 100644 diff --git a/assets/styles/misc.css b/assets/styles/misc.css old mode 100755 new mode 100644 diff --git a/assets/styles/org-syntax.css b/assets/styles/org-syntax.css old mode 100755 new mode 100644 diff --git a/assets/styles/style.css b/assets/styles/style.css old mode 100755 new mode 100644 diff --git a/assets/styles/wird-tracker.css b/assets/styles/wird-tracker.css old mode 100755 new mode 100644 diff --git a/assets/styles/zhd.css b/assets/styles/zhd.css old mode 100755 new mode 100644 diff --git a/backups/02-11-week-review.org~ b/backups/02-11-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/04-01-week-review.org~ b/backups/04-01-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/07-12-week-review.org~ b/backups/07-12-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/09-11-week-review.org~ b/backups/09-11-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/14-12-week-review.org~ b/backups/14-12-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/16-11-week-review.org~ b/backups/16-11-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/23-11-week-review.org~ b/backups/23-11-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/28-12-week-review.org~ b/backups/28-12-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/30-11-week-review.org~ b/backups/30-11-week-review.org~ old mode 100755 new mode 100644 diff --git a/backups/Makefile~ b/backups/Makefile~ old mode 100755 new mode 100644 diff --git a/backups/airflow.org~ b/backups/airflow.org~ old mode 100755 new mode 100644 diff --git a/backups/backlog.org~ b/backups/backlog.org~ old mode 100755 new mode 100644 diff --git a/backups/benefits-of-reading.org~ b/backups/benefits-of-reading.org~ old mode 100755 new mode 100644 diff --git a/backups/career-intro.org~ b/backups/career-intro.org~ old mode 100755 new mode 100644 diff --git a/backups/comments.js~ b/backups/comments.js~ old mode 100755 new mode 100644 diff --git a/backups/competency-status-board.js~ b/backups/competency-status-board.js~ old mode 100755 new mode 100644 diff --git a/backups/countdown.org~ b/backups/countdown.org~ old mode 100755 new mode 100644 diff --git a/backups/gallery-init.js~ b/backups/gallery-init.js~ old mode 100755 new mode 100644 diff --git a/backups/hilberts.hotel.org~ b/backups/hilberts.hotel.org~ old mode 100755 new mode 100644 diff --git a/backups/invest-principles.org~ b/backups/invest-principles.org~ old mode 100755 new mode 100644 diff --git a/backups/lean.org~ b/backups/lean.org~ old mode 100755 new mode 100644 diff --git a/backups/management-of-self.org~ b/backups/management-of-self.org~ old mode 100755 new mode 100644 diff --git a/backups/normalisation.org~ b/backups/normalisation.org~ old mode 100755 new mode 100644 diff --git a/backups/notes.js~ b/backups/notes.js~ old mode 100755 new mode 100644 diff --git a/backups/notes.org~ b/backups/notes.org~ old mode 100755 new mode 100644 diff --git a/backups/owasp.org~ b/backups/owasp.org~ old mode 100755 new mode 100644 diff --git a/backups/probation-objectives.org~ b/backups/probation-objectives.org~ old mode 100755 new mode 100644 diff --git a/backups/publish-pages.org~ b/backups/publish-pages.org~ old mode 100755 new mode 100644 diff --git a/backups/requirements-features.org~ b/backups/requirements-features.org~ old mode 100755 new mode 100644 diff --git a/backups/retrospectives.org~ b/backups/retrospectives.org~ old mode 100755 new mode 100644 diff --git a/backups/services.org~ b/backups/services.org~ old mode 100755 new mode 100644 diff --git a/backups/setup.org~ b/backups/setup.org~ old mode 100755 new mode 100644 diff --git a/backups/solid-principles.org~ b/backups/solid-principles.org~ old mode 100755 new mode 100644 diff --git a/backups/spending-the-whole-day-on-this-website.org~ b/backups/spending-the-whole-day-on-this-website.org~ old mode 100755 new mode 100644 diff --git a/backups/status-board.js~ b/backups/status-board.js~ old mode 100755 new mode 100644 diff --git a/backups/status.org~ b/backups/status.org~ old mode 100755 new mode 100644 diff --git a/backups/syntax.org~ b/backups/syntax.org~ old mode 100755 new mode 100644 diff --git a/backups/third-time.org~ b/backups/third-time.org~ old mode 100755 new mode 100644 diff --git a/backups/wacom-with-arch.org~ b/backups/wacom-with-arch.org~ old mode 100755 new mode 100644 diff --git a/backups/what-do-i-want-to-do-with-emacs.org~ b/backups/what-do-i-want-to-do-with-emacs.org~ old mode 100755 new mode 100644 diff --git a/backups/wireframe-designs.org~ b/backups/wireframe-designs.org~ old mode 100755 new mode 100644 diff --git a/backups/zettelkasten.org~ b/backups/zettelkasten.org~ old mode 100755 new mode 100644 diff --git a/blogs/2025/08-august/benefits-of-reading.org b/blogs/2025/08-august/benefits-of-reading.org old mode 100755 new mode 100644 diff --git a/blogs/2025/08-august/hilberts.hotel.org b/blogs/2025/08-august/hilberts.hotel.org old mode 100755 new mode 100644 diff --git a/blogs/2025/08-august/spending-the-whole-day-on-this-website.org b/blogs/2025/08-august/spending-the-whole-day-on-this-website.org old mode 100755 new mode 100644 diff --git a/blogs/2025/08-august/third-time.org b/blogs/2025/08-august/third-time.org old mode 100755 new mode 100644 diff --git a/blogs/2025/08-august/wacom-with-arch.org b/blogs/2025/08-august/wacom-with-arch.org old mode 100755 new mode 100644 diff --git a/blogs/2025/08-august/what-do-i-want-to-do-with-emacs.org b/blogs/2025/08-august/what-do-i-want-to-do-with-emacs.org old mode 100755 new mode 100644 diff --git a/blogs/2025/08-august/zettelkasten.org b/blogs/2025/08-august/zettelkasten.org old mode 100755 new mode 100644 diff --git a/blogs/2025/11-november/02-11-week-review.org b/blogs/2025/11-november/02-11-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/11-november/09-11-week-review.org b/blogs/2025/11-november/09-11-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/11-november/16-11-week-review.org b/blogs/2025/11-november/16-11-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/11-november/23-11-week-review.org b/blogs/2025/11-november/23-11-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/11-november/30-11-week-review.org b/blogs/2025/11-november/30-11-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/12-december/07-12-week-review.org b/blogs/2025/12-december/07-12-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/12-december/14-12-week-review.org b/blogs/2025/12-december/14-12-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/12-december/21-12-week-review.org b/blogs/2025/12-december/21-12-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2025/12-december/28-12-week-review.org b/blogs/2025/12-december/28-12-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/01-january/04-01-week-review.org b/blogs/2026/01-january/04-01-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/01-january/11-01-week-review.org b/blogs/2026/01-january/11-01-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/01-january/18-01-week-review.org b/blogs/2026/01-january/18-01-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/01-january/25-01-week-review.org b/blogs/2026/01-january/25-01-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/01-02-week-review.org b/blogs/2026/02-february/01-02-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/08-02-week-review.org b/blogs/2026/02-february/08-02-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/15-02-week-review.org b/blogs/2026/02-february/15-02-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/22-02-week-review.org b/blogs/2026/02-february/22-02-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/24-02-26.org b/blogs/2026/02-february/24-02-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/26-02-26.org b/blogs/2026/02-february/26-02-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/27-02-26.org b/blogs/2026/02-february/27-02-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/02-february/third-meeting.org b/blogs/2026/02-february/third-meeting.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/01-03-week-review.org b/blogs/2026/03-march/01-03-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/08-03-week-review.org b/blogs/2026/03-march/08-03-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/15-03-week-review.org b/blogs/2026/03-march/15-03-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/22-03-week-review.org b/blogs/2026/03-march/22-03-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/29-03-week-review.org b/blogs/2026/03-march/29-03-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/cooking-dinner-29-03-26.org b/blogs/2026/03-march/cooking-dinner-29-03-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/feeling-sleepy.org b/blogs/2026/03-march/feeling-sleepy.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/fixing-the-dag-18-03.org b/blogs/2026/03-march/fixing-the-dag-18-03.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/installing-emacs-23-03.org b/blogs/2026/03-march/installing-emacs-23-03.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/intellectually-challenging-myself-30-03-26.org b/blogs/2026/03-march/intellectually-challenging-myself-30-03-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/more-thoughts-regarding-changes-26-03-26.org b/blogs/2026/03-march/more-thoughts-regarding-changes-26-03-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/oversleeping-16-03.org b/blogs/2026/03-march/oversleeping-16-03.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/quick-lunch-24-03.org b/blogs/2026/03-march/quick-lunch-24-03.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/setting-a-wird-tracker-19-03.org b/blogs/2026/03-march/setting-a-wird-tracker-19-03.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/thoughts-on-life-25-03-26.org b/blogs/2026/03-march/thoughts-on-life-25-03-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/03-march/training-on-friday-afternoon-27-03-26.org b/blogs/2026/03-march/training-on-friday-afternoon-27-03-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/05-04-week-review.org b/blogs/2026/04-april/05-04-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/12-04-week-review.org b/blogs/2026/04-april/12-04-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/16-04-26.org b/blogs/2026/04-april/16-04-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/19-04-week-review.org b/blogs/2026/04-april/19-04-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/26-04-week-review.org b/blogs/2026/04-april/26-04-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/action-plan-07-04-26.org b/blogs/2026/04-april/action-plan-07-04-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/adding-more-wird-22-04-26.org b/blogs/2026/04-april/adding-more-wird-22-04-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/april-almost-over-28-04.org b/blogs/2026/04-april/april-almost-over-28-04.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/comparison-14-04.org b/blogs/2026/04-april/comparison-14-04.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/ending-the-week-17-04-26.org b/blogs/2026/04-april/ending-the-week-17-04-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/joining-new-team-meeting-02-04-2026.org b/blogs/2026/04-april/joining-new-team-meeting-02-04-2026.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/making-notes-through-org-noter-06-05.org b/blogs/2026/04-april/making-notes-through-org-noter-06-05.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.org b/blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/starting-new-rotation-08-04-26.org b/blogs/2026/04-april/starting-new-rotation-08-04-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/weekly-target-06-04-26.org b/blogs/2026/04-april/weekly-target-06-04-26.org old mode 100755 new mode 100644 diff --git a/blogs/2026/04-april/wise-words-29-04.org b/blogs/2026/04-april/wise-words-29-04.org old mode 100755 new mode 100644 diff --git a/blogs/2026/05-may/03-05-week-review.org b/blogs/2026/05-may/03-05-week-review.org old mode 100755 new mode 100644 diff --git a/blogs/2026/05-may/sun-soak-01-05.org b/blogs/2026/05-may/sun-soak-01-05.org old mode 100755 new mode 100644 diff --git a/blogs/blogs-intro.org b/blogs/blogs-intro.org old mode 100755 new mode 100644 diff --git a/blogs/blogs-list.org b/blogs/blogs-list.org old mode 100755 new mode 100644 diff --git a/blogs/publish-pages.org b/blogs/publish-pages.org old mode 100755 new mode 100644 diff --git a/books/books-list.org b/books/books-list.org old mode 100755 new mode 100644 diff --git a/books/clean-code/clean-code-notes.org b/books/clean-code/clean-code-notes.org old mode 100755 new mode 100644 diff --git a/build-site-tests.el b/build-site-tests.el old mode 100755 new mode 100644 diff --git a/build-site.el b/build-site.el old mode 100755 new mode 100644 diff --git a/gitea-build-monitor.ps1 b/gitea-build-monitor.ps1 new file mode 100644 index 0000000..79aab8d --- /dev/null +++ b/gitea-build-monitor.ps1 @@ -0,0 +1,704 @@ +<# +.SYNOPSIS + Monitor recent Gitea Actions build jobs and notify Discord about failures, warnings, errors, or slow builds. + +.DESCRIPTION + This script is intended to run after the main build pipeline. It queries a self-hosted Gitea instance, + analyzes recent Actions jobs for one or more repositories, parses logs where available, caches processed + job IDs locally, and sends grouped Discord embed notifications. + + Required token permissions depend on your Gitea version and repository visibility. For private repos, + use a personal access token that can read repository Actions/jobs and logs. + + Sample Discord payload structure: + { + "username": "Gitea Build Monitor", + "embeds": [ + { + "title": "CRITICAL: owner/repo build #123", + "url": "http://127.0.0.1:3000/owner/repo/actions/runs/123", + "color": 15158332, + "fields": [ + { "name": "Status", "value": "❌ failure", "inline": true }, + { "name": "Branch", "value": "main", "inline": true }, + { "name": "Commit", "value": "`abcdef12` Fix build", "inline": false } + ] + } + ] + } +#> + +param( + [switch]$DryRun, + [switch]$ValidateOnly +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# ========================= +# Example configuration +# ========================= +$Config = @{ + GiteaBaseUrl = 'http://127.0.0.1:3000' + ApiToken = '328ae2196703fc988e9c21962341ba88709511ad' + DiscordWebhookUrl = 'https://discord.com/api/webhooks/1501605890581856430/j3zeMhCW-V583awzqhyrZMfVwzZl-RRCLdIa32jUiymbodzpu9WZaX_p3HiiQMtUUO0m' + + # Supports multiple repositories. + Repositories = @( + @{ Owner = 'zaine'; Name = 'org_web' } + ) + + LookbackMinutes = 30 + SlowBuildThresholdMins = 20 + CacheFile = Join-Path $PSScriptRoot 'gitea-build-monitor-cache.json' + LogFile = Join-Path $PSScriptRoot 'gitea-build-monitor.log' + MaxJobsPerRepository = 50 + MaxLogBytesToAnalyze = 250000 + MaxDiscordEmbeds = 10 + RetryCount = 3 + RetryDelaySeconds = 2 + + # Tune these patterns to your build tooling. + ErrorPatterns = @( + '(?im)\b(error|exception|fatal|failed|failure)\b', + '(?im)\bbuild failed\b', + '(?im)\btest(s)? failed\b' + ) + WarningPatterns = @( + '(?im)\bwarning\b', + '(?im)\bdeprecated\b' + ) + + # When true, successful but slow/warned jobs are reported too. + NotifyWarningsAndSlow = $true +} + +function Write-Log { + param( + [Parameter(Mandatory)] + [string]$Message, + + [ValidateSet('INFO', 'WARN', 'ERROR')] + [string]$Level = 'INFO' + ) + + $line = '{0} [{1}] {2}' -f (Get-Date).ToString('o'), $Level, $Message + Write-Host $line + Add-Content -Path $Config.LogFile -Value $line +} + +function Assert-Config { + if ([string]::IsNullOrWhiteSpace($Config.ApiToken)) { + throw 'Missing Gitea API token. Set $Config.ApiToken or the GITEA_API_TOKEN environment variable.' + } + if ([string]::IsNullOrWhiteSpace($Config.DiscordWebhookUrl)) { + throw 'Missing Discord webhook URL. Set $Config.DiscordWebhookUrl or the DISCORD_WEBHOOK_URL environment variable.' + } + if (-not $Config.Repositories -or $Config.Repositories.Count -eq 0) { + throw 'Configure at least one repository in $Config.Repositories.' + } +} + +function Join-Url { + param( + [Parameter(Mandatory)][string]$Base, + [Parameter(Mandatory)][string]$Path + ) + + '{0}/{1}' -f $Base.TrimEnd('/'), $Path.TrimStart('/') +} + +function Invoke-GiteaApi { + param( + [Parameter(Mandatory)][string]$Path, + [ValidateSet('GET', 'POST')][string]$Method = 'GET', + [object]$Body = $null, + [switch]$RawText + ) + + $uri = Join-Url -Base $Config.GiteaBaseUrl -Path $Path + $headers = @{ + Authorization = "token $($Config.ApiToken)" + Accept = if ($RawText) { 'text/plain' } else { 'application/json' } + } + + for ($attempt = 1; $attempt -le [int]$Config.RetryCount; $attempt++) { + try { + $parameters = @{ + Uri = $uri + Method = $Method + Headers = $headers + ErrorAction = 'Stop' + } + + if ($null -ne $Body) { + $parameters.Body = ($Body | ConvertTo-Json -Depth 10) + $parameters.ContentType = 'application/json' + } + + return Invoke-RestMethod @parameters + } + catch { + $status = $null + if ($_.Exception.Response -and $_.Exception.Response.StatusCode) { + $status = [int]$_.Exception.Response.StatusCode + } + + Write-Log -Level 'WARN' -Message ("Gitea API call failed, attempt {0}/{1}, status {2}: {3}" -f $attempt, $Config.RetryCount, $status, $_.Exception.Message) + if ($attempt -ge [int]$Config.RetryCount) { + throw + } + Start-Sleep -Seconds ([int]$Config.RetryDelaySeconds * $attempt) + } + } +} + +function Invoke-DiscordWebhook { + param([Parameter(Mandatory)][hashtable]$Payload) + + if ($DryRun) { + Write-Log -Message "Dry run enabled; Discord payload was not sent: $($Payload | ConvertTo-Json -Depth 12 -Compress)" + return + } + + for ($attempt = 1; $attempt -le [int]$Config.RetryCount; $attempt++) { + try { + Invoke-RestMethod -Uri $Config.DiscordWebhookUrl -Method POST -ContentType 'application/json' -Body ($Payload | ConvertTo-Json -Depth 12) -ErrorAction Stop | Out-Null + return + } + catch { + Write-Log -Level 'WARN' -Message ("Discord webhook failed, attempt {0}/{1}: {2}" -f $attempt, $Config.RetryCount, $_.Exception.Message) + if ($attempt -ge [int]$Config.RetryCount) { + throw + } + Start-Sleep -Seconds ([int]$Config.RetryDelaySeconds * $attempt) + } + } +} + +function Read-ProcessedCache { + if (-not (Test-Path -Path $Config.CacheFile)) { + return @{} + } + + try { + $json = Get-Content -Path $Config.CacheFile -Raw + if ([string]::IsNullOrWhiteSpace($json)) { + return @{} + } + + $data = $json | ConvertFrom-Json + $cache = @{} + foreach ($property in $data.PSObject.Properties) { + $cache[$property.Name] = [datetime]$property.Value + } + return $cache + } + catch { + Write-Log -Level 'WARN' -Message "Could not read cache file, starting with an empty cache: $($_.Exception.Message)" + return @{} + } +} + +function Save-ProcessedCache { + param([Parameter(Mandatory)][hashtable]$Cache) + + $cutoff = (Get-Date).AddDays(-14) + $trimmed = @{} + foreach ($key in $Cache.Keys) { + if ([datetime]$Cache[$key] -ge $cutoff) { + $trimmed[$key] = ([datetime]$Cache[$key]).ToString('o') + } + } + + $directory = Split-Path -Parent $Config.CacheFile + if ($directory -and -not (Test-Path -Path $directory)) { + New-Item -ItemType Directory -Path $directory -Force | Out-Null + } + + $trimmed | ConvertTo-Json -Depth 5 | Set-Content -Path $Config.CacheFile -Encoding UTF8 +} + +function Get-RepositoryOwner { + param([Parameter(Mandatory)][hashtable]$Repository) + return [string]$Repository['Owner'] +} + +function Get-RepositoryName { + param([Parameter(Mandatory)][hashtable]$Repository) + return [string]$Repository['Name'] +} + +function Get-RepositorySlug { + param([Parameter(Mandatory)][hashtable]$Repository) + return '{0}/{1}' -f (Get-RepositoryOwner -Repository $Repository), (Get-RepositoryName -Repository $Repository) +} + +function Get-BuildRuns { + param( + [Parameter(Mandatory)][hashtable]$Repository + ) + + $owner = [uri]::EscapeDataString((Get-RepositoryOwner -Repository $Repository)) + $repo = [uri]::EscapeDataString((Get-RepositoryName -Repository $Repository)) + $path = "/api/v1/repos/$owner/$repo/actions/jobs?page=1&limit=$($Config.MaxJobsPerRepository)" + $response = Invoke-GiteaApi -Path $path + + $jobs = Get-ObjectValue -Object $response -Name 'jobs' + if ($jobs) { + return @($jobs) + } + + return @() +} + +function Get-CommitMessage { + param( + [Parameter(Mandatory)][hashtable]$Repository, + [Parameter(Mandatory)][string]$Sha + ) + + if ([string]::IsNullOrWhiteSpace($Sha)) { + return '' + } + + try { + $owner = [uri]::EscapeDataString((Get-RepositoryOwner -Repository $Repository)) + $repo = [uri]::EscapeDataString((Get-RepositoryName -Repository $Repository)) + $encodedSha = [uri]::EscapeDataString($Sha) + $commit = Invoke-GiteaApi -Path "/api/v1/repos/$owner/$repo/git/commits/$encodedSha" + + $message = Get-ObjectValue -Object $commit -Name 'message' -Default '' + if ($message) { + return (($message -split "`r?`n")[0]).Trim() + } + } + catch { + Write-Log -Level 'WARN' -Message "Could not fetch commit message for $(Get-RepositorySlug -Repository $Repository) $($Sha): $($_.Exception.Message)" + } + + return '' +} + +function Get-JobLogText { + param( + [Parameter(Mandatory)][hashtable]$Repository, + [Parameter(Mandatory)][object]$Job + ) + + try { + $owner = [uri]::EscapeDataString((Get-RepositoryOwner -Repository $Repository)) + $repo = [uri]::EscapeDataString((Get-RepositoryName -Repository $Repository)) + $jobId = [uri]::EscapeDataString([string](Get-ObjectValue -Object $Job -Name 'id')) + $log = Invoke-GiteaApi -Path "/api/v1/repos/$owner/$repo/actions/jobs/$jobId/logs" -RawText + + if ($null -eq $log) { + return '' + } + + $text = [string]$log + if ($text.Length -gt [int]$Config.MaxLogBytesToAnalyze) { + return $text.Substring([Math]::Max(0, $text.Length - [int]$Config.MaxLogBytesToAnalyze)) + } + return $text + } + catch { + Write-Log -Level 'WARN' -Message "Could not fetch logs for job $(Get-ObjectValue -Object $Job -Name 'id'): $($_.Exception.Message)" + return '' + } +} + +function Get-Duration { + param([Parameter(Mandatory)][object]$Job) + + $start = $null + $end = $null + + $startedAt = Get-ObjectValue -Object $Job -Name 'started_at' + $createdAt = Get-ObjectValue -Object $Job -Name 'created_at' + $completedAt = Get-ObjectValue -Object $Job -Name 'completed_at' + + if ($startedAt) { + $start = [datetime]$startedAt + } + elseif ($createdAt) { + $start = [datetime]$createdAt + } + + if ($completedAt) { + $end = [datetime]$completedAt + } + else { + $end = Get-Date + } + + if ($null -eq $start) { + return [timespan]::Zero + } + + return ($end.ToUniversalTime() - $start.ToUniversalTime()) +} + +function Get-ObjectValue { + param( + [Parameter(Mandatory)][object]$Object, + [Parameter(Mandatory)][string]$Name, + [object]$Default = $null + ) + + if ($null -eq $Object) { + return $Default + } + + if ($Object -is [hashtable] -and $Object.ContainsKey($Name)) { + return $Object[$Name] + } + + $property = $Object.PSObject.Properties[$Name] + if ($property) { + return $property.Value + } + + return $Default +} + +function Format-Duration { + param([Parameter(Mandatory)][timespan]$Duration) + + if ($Duration.TotalHours -ge 1) { + return '{0}h {1}m {2}s' -f [int]$Duration.TotalHours, $Duration.Minutes, $Duration.Seconds + } + if ($Duration.TotalMinutes -ge 1) { + return '{0}m {1}s' -f [int]$Duration.TotalMinutes, $Duration.Seconds + } + return '{0}s' -f [int]$Duration.TotalSeconds +} + +function Get-LogSummary { + param([Parameter(Mandatory)][string]$LogText) + + if ([string]::IsNullOrWhiteSpace($LogText)) { + return @{ + HasErrors = $false + HasWarnings = $false + Summary = 'No log summary available.' + } + } + + $matchedLines = New-Object System.Collections.Generic.List[string] + $hasErrors = $false + $hasWarnings = $false + + foreach ($line in ($LogText -split "`r?`n")) { + $trimmed = $line.Trim() + if ([string]::IsNullOrWhiteSpace($trimmed)) { + continue + } + + $isError = $false + foreach ($pattern in $Config.ErrorPatterns) { + if ($trimmed -match $pattern) { + $isError = $true + $hasErrors = $true + break + } + } + + $isWarning = $false + foreach ($pattern in $Config.WarningPatterns) { + if ($trimmed -match $pattern) { + $isWarning = $true + $hasWarnings = $true + break + } + } + + if (($isError -or $isWarning) -and $matchedLines.Count -lt 8) { + $matchedLines.Add($trimmed) + } + } + + $summary = if ($matchedLines.Count -gt 0) { + ($matchedLines | Select-Object -First 8) -join "`n" + } + else { + 'No matching warning or error lines found.' + } + + return @{ + HasErrors = $hasErrors + HasWarnings = $hasWarnings + Summary = $summary + } +} + +function Get-Severity { + param( + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][bool]$HasErrors, + [Parameter(Mandatory)][bool]$HasWarnings, + [Parameter(Mandatory)][bool]$IsSlow + ) + + $normalized = $Status.ToLowerInvariant() + if ($normalized -in @('failure', 'failed', 'cancelled', 'timed_out') -or $HasErrors) { + return 'Critical' + } + if ($IsSlow -or $HasWarnings) { + return 'Warning' + } + return 'Info' +} + +function Analyze-Build { + param( + [Parameter(Mandatory)][hashtable]$Repository, + [Parameter(Mandatory)][object]$Job + ) + + $conclusion = Get-ObjectValue -Object $Job -Name 'conclusion' + $jobStatus = Get-ObjectValue -Object $Job -Name 'status' + $status = if ($conclusion) { [string]$conclusion } elseif ($jobStatus) { [string]$jobStatus } else { 'unknown' } + $duration = Get-Duration -Job $Job + $isSlow = $duration.TotalMinutes -ge [double]$Config.SlowBuildThresholdMins + $logText = '' + + $definitelyProblematicStatus = $status.ToLowerInvariant() -in @('failure', 'failed', 'cancelled', 'timed_out') + if ($definitelyProblematicStatus -or $Config.NotifyWarningsAndSlow) { + $logText = Get-JobLogText -Repository $Repository -Job $Job + } + + $logSummary = Get-LogSummary -LogText $logText + $severity = Get-Severity -Status $status -HasErrors $logSummary.HasErrors -HasWarnings $logSummary.HasWarnings -IsSlow $isSlow + + $shouldAlert = $severity -ne 'Info' + if (-not $Config.NotifyWarningsAndSlow -and $severity -eq 'Warning') { + $shouldAlert = $false + } + + $sha = [string](Get-ObjectValue -Object $Job -Name 'head_sha' -Default '') + $commitMessage = Get-CommitMessage -Repository $Repository -Sha $sha + $runId = [string](Get-ObjectValue -Object $Job -Name 'run_id' -Default (Get-ObjectValue -Object $Job -Name 'id')) + $htmlUrl = [string](Get-ObjectValue -Object $Job -Name 'html_url' -Default '') + $runUrl = [string](Get-ObjectValue -Object $Job -Name 'run_url' -Default '') + $repositorySlug = Get-RepositorySlug -Repository $Repository + $buildUrl = if ($htmlUrl) { $htmlUrl } elseif ($runUrl) { $runUrl } else { Join-Url -Base $Config.GiteaBaseUrl -Path "$repositorySlug/actions/runs/$runId" } + $startedAt = Get-ObjectValue -Object $Job -Name 'started_at' + $createdAt = Get-ObjectValue -Object $Job -Name 'created_at' + + return [pscustomobject]@{ + Repository = $repositorySlug + Owner = Get-RepositoryOwner -Repository $Repository + Repo = Get-RepositoryName -Repository $Repository + JobId = [string](Get-ObjectValue -Object $Job -Name 'id') + RunId = $runId + Branch = [string](Get-ObjectValue -Object $Job -Name 'head_branch' -Default 'unknown') + CommitSha = $sha + CommitMessage = $commitMessage + Status = $status + Duration = $duration + DurationText = Format-Duration -Duration $duration + BuildUrl = $buildUrl + ErrorSummary = $logSummary.Summary + HasErrors = $logSummary.HasErrors + HasWarnings = $logSummary.HasWarnings + IsSlow = $isSlow + Severity = $severity + ShouldAlert = $shouldAlert + StartedAt = if ($startedAt) { [datetime]$startedAt } elseif ($createdAt) { [datetime]$createdAt } else { Get-Date } + } +} + +function New-DiscordEmbed { + param([Parameter(Mandatory)][object]$Analysis) + + $emoji = switch ($Analysis.Severity) { + 'Critical' { '❌' } + 'Warning' { '⚠️' } + default { 'ℹ️' } + } + + $color = switch ($Analysis.Severity) { + 'Critical' { 15158332 } + 'Warning' { 16776960 } + default { 3447003 } + } + + $shortSha = if ($Analysis.CommitSha.Length -ge 8) { $Analysis.CommitSha.Substring(0, 8) } else { $Analysis.CommitSha } + $commitValue = if ($shortSha) { + if ($Analysis.CommitMessage) { "``$shortSha`` $($Analysis.CommitMessage)" } else { "``$shortSha``" } + } + else { + 'unknown' + } + + $flags = @() + if ($Analysis.HasErrors) { $flags += 'errors found in logs' } + if ($Analysis.HasWarnings) { $flags += 'warnings found in logs' } + if ($Analysis.IsSlow) { $flags += "duration exceeded $($Config.SlowBuildThresholdMins)m" } + if ($flags.Count -eq 0) { $flags += 'problematic status' } + + $summary = $Analysis.ErrorSummary + if ($summary.Length -gt 950) { + $summary = $summary.Substring(0, 947) + '...' + } + + return @{ + title = "$emoji $($Analysis.Severity): $($Analysis.Repository) build #$($Analysis.RunId)" + url = $Analysis.BuildUrl + color = $color + description = ($flags -join ', ') + fields = @( + @{ name = 'Status'; value = "$emoji $($Analysis.Status)"; inline = $true }, + @{ name = 'Branch'; value = $Analysis.Branch; inline = $true }, + @{ name = 'Duration'; value = $Analysis.DurationText; inline = $true }, + @{ name = 'Commit'; value = $commitValue; inline = $false }, + @{ name = 'Summary'; value = $summary; inline = $false } + ) + footer = @{ + text = "Job ID $($Analysis.JobId) | Run ID $($Analysis.RunId)" + } + timestamp = (Get-Date).ToUniversalTime().ToString('o') + } +} + +function Send-DiscordNotification { + param([Parameter(Mandatory)][object[]]$Analyses) + + $alerting = @($Analyses | Where-Object { $_.ShouldAlert } | Sort-Object Severity, Repository, RunId) + if ($alerting.Count -eq 0) { + Write-Log -Message 'No alertable builds found.' + return + } + + $maxEmbeds = [int]$Config.MaxDiscordEmbeds + if ($maxEmbeds -lt 1) { + $maxEmbeds = 1 + } + for ($offset = 0; $offset -lt $alerting.Count; $offset += $maxEmbeds) { + $chunk = @($alerting | Select-Object -Skip $offset -First $maxEmbeds) + $criticalCount = @($chunk | Where-Object { $_.Severity -eq 'Critical' }).Count + $warningCount = @($chunk | Where-Object { $_.Severity -eq 'Warning' }).Count + $embeds = @($chunk | ForEach-Object { New-DiscordEmbed -Analysis $_ }) + $payload = @{ + username = 'Gitea Build Monitor' + content = if ($criticalCount -gt 0) { "❌ Gitea build monitor found $criticalCount critical issue(s) and $warningCount warning(s)." } else { "⚠️ Gitea build monitor found $warningCount warning(s)." } + embeds = $embeds + } + + Invoke-DiscordWebhook -Payload $payload + if ($DryRun) { + Write-Log -Message "Prepared Discord notification with $($chunk.Count) embed(s) in dry-run mode." + } + else { + Write-Log -Message "Sent Discord notification with $($chunk.Count) embed(s)." + } + } +} + +function Test-IsRecentBuild { + param([Parameter(Mandatory)][object]$Job) + + $cutoff = (Get-Date).ToUniversalTime().AddMinutes(-[int]$Config.LookbackMinutes) + $candidateDates = @( + Get-ObjectValue -Object $Job -Name 'started_at' + Get-ObjectValue -Object $Job -Name 'completed_at' + Get-ObjectValue -Object $Job -Name 'created_at' + ) | Where-Object { $_ } + foreach ($date in $candidateDates) { + if (([datetime]$date).ToUniversalTime() -ge $cutoff) { + return $true + } + } + return $false +} + +function Get-CacheKey { + param( + [Parameter(Mandatory)][hashtable]$Repository, + [Parameter(Mandatory)][object]$Job + ) + + $jobId = Get-ObjectValue -Object $Job -Name 'id' + $conclusion = Get-ObjectValue -Object $Job -Name 'conclusion' + $status = Get-ObjectValue -Object $Job -Name 'status' -Default 'unknown' + $state = if ($conclusion) { $conclusion } else { $status } + + return "$(Get-RepositorySlug -Repository $Repository):job:$jobId`:state:$state" +} + +function Start-BuildMonitor { + Assert-Config + + $logDirectory = Split-Path -Parent $Config.LogFile + if ($logDirectory -and -not (Test-Path -Path $logDirectory)) { + New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null + } + + if ($ValidateOnly) { + Write-Log -Message 'Validation mode completed: configuration and PowerShell runtime are valid.' + return + } + + $cache = if ($DryRun) { @{} } else { Read-ProcessedCache } + $analyses = New-Object System.Collections.Generic.List[object] + + foreach ($repository in $Config.Repositories) { + Write-Log -Message "Fetching recent Gitea Actions jobs for $(Get-RepositorySlug -Repository $repository)." + + try { + $jobs = Get-BuildRuns -Repository $repository + } + catch { + Write-Log -Level 'ERROR' -Message "Failed to retrieve jobs for $(Get-RepositorySlug -Repository $repository): $($_.Exception.Message)" + continue + } + + foreach ($job in $jobs) { + if (-not (Test-IsRecentBuild -Job $job)) { + continue + } + + $cacheKey = Get-CacheKey -Repository $repository -Job $job + if ($cache.ContainsKey($cacheKey)) { + continue + } + + try { + $analysis = Analyze-Build -Repository $repository -Job $job + $analyses.Add($analysis) + if (-not $DryRun) { + $cache[$cacheKey] = Get-Date + } + Write-Log -Message ("Analyzed job {0} for {1}: severity={2}, status={3}, duration={4}" -f (Get-ObjectValue -Object $job -Name 'id'), $analysis.Repository, $analysis.Severity, $analysis.Status, $analysis.DurationText) + } + catch { + Write-Log -Level 'ERROR' -Message "Failed to analyze job $(Get-ObjectValue -Object $job -Name 'id'): $($_.Exception.Message)" + } + } + } + + if ($analyses.Count -gt 0) { + Send-DiscordNotification -Analyses $analyses.ToArray() + } + else { + Write-Log -Message "No new jobs found in the last $($Config.LookbackMinutes) minute(s)." + } + + if (-not $DryRun) { + Save-ProcessedCache -Cache $cache + } +} + +try { + Start-BuildMonitor +} +catch { + Write-Log -Level 'ERROR' -Message "Build monitor failed: $($_.Exception.Message)" + Write-Log -Level 'ERROR' -Message "Exception type: $($_.Exception.GetType().FullName)" + if ($_.ScriptStackTrace) { + Write-Log -Level 'ERROR' -Message "Stack trace: $($_.ScriptStackTrace)" + } + exit 1 +} diff --git a/home/backlog.org b/home/backlog.org old mode 100755 new mode 100644 diff --git a/home/categories.org b/home/categories.org old mode 100755 new mode 100644 diff --git a/home/categories.sync-conflict-20260417-233429-VT6366A.org b/home/categories.sync-conflict-20260417-233429-VT6366A.org old mode 100755 new mode 100644 diff --git a/home/contact.org b/home/contact.org old mode 100755 new mode 100644 diff --git a/home/countdown.org b/home/countdown.org old mode 100755 new mode 100644 diff --git a/home/guide/setup.org b/home/guide/setup.org old mode 100755 new mode 100644 diff --git a/home/guide/wird-tracker-guide.org b/home/guide/wird-tracker-guide.org old mode 100755 new mode 100644 diff --git a/home/notes.org b/home/notes.org old mode 100755 new mode 100644 diff --git a/home/services.org b/home/services.org old mode 100755 new mode 100644 diff --git a/home/status.org b/home/status.org old mode 100755 new mode 100644 diff --git a/home/wird-tracker.org b/home/wird-tracker.org old mode 100755 new mode 100644 diff --git a/index.org b/index.org old mode 100755 new mode 100644 diff --git a/lima/index.md b/lima/index.md old mode 100755 new mode 100644 diff --git a/lima/lima-list.org b/lima/lima-list.org old mode 100755 new mode 100644 diff --git a/lisp/recently-updated.el b/lisp/recently-updated.el old mode 100755 new mode 100644 diff --git a/lisp/sidenotes.el b/lisp/sidenotes.el old mode 100755 new mode 100644 diff --git a/lisp/sitemaps.el b/lisp/sitemaps.el old mode 100755 new mode 100644 diff --git a/lisp/tags.el b/lisp/tags.el old mode 100755 new mode 100644 diff --git a/org-web-build.log b/org-web-build.log old mode 100755 new mode 100644 diff --git a/posts/career/airflow.org b/posts/career/airflow.org old mode 100755 new mode 100644 diff --git a/posts/career/career-intro.org b/posts/career/career-intro.org old mode 100755 new mode 100644 diff --git a/posts/career/career-list.org b/posts/career/career-list.org old mode 100755 new mode 100644 diff --git a/posts/career/database-permissions.org b/posts/career/database-permissions.org old mode 100755 new mode 100644 diff --git a/posts/career/ha-dr.org b/posts/career/ha-dr.org old mode 100755 new mode 100644 diff --git a/posts/career/invest-principles.org b/posts/career/invest-principles.org old mode 100755 new mode 100644 diff --git a/posts/career/javascript.org b/posts/career/javascript.org old mode 100755 new mode 100644 diff --git a/posts/career/lean.org b/posts/career/lean.org old mode 100755 new mode 100644 diff --git a/posts/career/management-of-self.org b/posts/career/management-of-self.org old mode 100755 new mode 100644 diff --git a/posts/career/monitoring-and-logging.org b/posts/career/monitoring-and-logging.org old mode 100755 new mode 100644 diff --git a/posts/career/normalisation.org b/posts/career/normalisation.org old mode 100755 new mode 100644 diff --git a/posts/career/owasp.org b/posts/career/owasp.org old mode 100755 new mode 100644 diff --git a/posts/career/pipelines.org b/posts/career/pipelines.org old mode 100755 new mode 100644 diff --git a/posts/career/probation-objectives.org b/posts/career/probation-objectives.org old mode 100755 new mode 100644 diff --git a/posts/career/requirements-features.org b/posts/career/requirements-features.org old mode 100755 new mode 100644 diff --git a/posts/career/restful-api.org b/posts/career/restful-api.org old mode 100755 new mode 100644 diff --git a/posts/career/retrospectives.org b/posts/career/retrospectives.org old mode 100755 new mode 100644 diff --git a/posts/career/solid-principles.org b/posts/career/solid-principles.org old mode 100755 new mode 100644 diff --git a/posts/career/wireframe-designs.org b/posts/career/wireframe-designs.org old mode 100755 new mode 100644 diff --git a/posts/posts-intro.org b/posts/posts-intro.org old mode 100755 new mode 100644 diff --git a/posts/posts-list.org b/posts/posts-list.org old mode 100755 new mode 100644 diff --git a/posts/posts-list.sync-conflict-20260417-233432-VT6366A.org b/posts/posts-list.sync-conflict-20260417-233432-VT6366A.org old mode 100755 new mode 100644 diff --git a/publish-and-push.sh b/publish-and-push.sh old mode 100755 new mode 100644 diff --git a/recently-updated.org b/recently-updated.org old mode 100755 new mode 100644 diff --git a/recently-updated.sync-conflict-20260417-233430-VT6366A.org b/recently-updated.sync-conflict-20260417-233430-VT6366A.org old mode 100755 new mode 100644 diff --git a/requirements.txt b/requirements.txt old mode 100755 new mode 100644 diff --git a/search-index-json.py b/search-index-json.py old mode 100755 new mode 100644 diff --git a/sitemap.org b/sitemap.org old mode 100755 new mode 100644 diff --git a/tags/education.org b/tags/education.org old mode 100755 new mode 100644 diff --git a/tags/emacs.org b/tags/emacs.org old mode 100755 new mode 100644 diff --git a/tags/insights.org b/tags/insights.org old mode 100755 new mode 100644 diff --git a/tags/introduction.org b/tags/introduction.org old mode 100755 new mode 100644 diff --git a/tags/learning.org b/tags/learning.org old mode 100755 new mode 100644 diff --git a/tags/life.org b/tags/life.org old mode 100755 new mode 100644 diff --git a/tags/life.sync-conflict-20260417-233423-VT6366A.org b/tags/life.sync-conflict-20260417-233423-VT6366A.org old mode 100755 new mode 100644 diff --git a/tags/maths.org b/tags/maths.org old mode 100755 new mode 100644 diff --git a/tags/notes.org b/tags/notes.org old mode 100755 new mode 100644 diff --git a/tags/reading.org b/tags/reading.org old mode 100755 new mode 100644 diff --git a/tags/review.org b/tags/review.org old mode 100755 new mode 100644 diff --git a/tags/review.sync-conflict-20260328-203248-VT6366A.org b/tags/review.sync-conflict-20260328-203248-VT6366A.org old mode 100755 new mode 100644 diff --git a/tags/update.org b/tags/update.org old mode 100755 new mode 100644 diff --git a/tags/website.org b/tags/website.org old mode 100755 new mode 100644 diff --git a/watcher.sh b/watcher.sh old mode 100755 new mode 100644 diff --git a/wip.org b/wip.org old mode 100755 new mode 100644