Skip to content

v1.9.2

Latest

Choose a tag to compare

@github-actions github-actions released this 14 Aug 06:08
· 171 commits to main since this release
057b439

Security

  • Closed a CodeQL js/xss-through-dom finding (alert #2) in the epic colour picker — renderTagRow()'s quick colour picker (src/js/02-utils.js) read the raw <input type="color"> value and wrote it straight into cat.color (persisted state) and the swatch's style.background, bypassing getCat()'s existing safeCssColor() sanitisation entirely; the innerHTML sink for the picker's own value attribute relied on that same upstream sanitisation, which CodeQL's dataflow analysis doesn't credit as a barrier across the object spread. All three sites now call safeCssColor() explicitly. A real <input type="color"> clamps its .value to valid hex at the DOM level, so this isn't exploitable through normal use today — it closes the gap CodeQL flags and guards against any future code path (e.g. a non-native picker, or a refactor) that could feed an unclamped value through the same handlers. 3 new regression tests in test/unit.mjs (VM-sandboxed, since a real browser can't hold an invalid value in a colour input to prove the "before" case). This alert had been open since PR #87 (2026-05); a prior attempt (PR #265) was closed as superseded without merging.

Removed

  • Dead V5 timer-bar CSS in _timer.scss — the .timer-bar layout (circle ticker, task-name block, action pills, note row, utility pills, paused-state variants, and the .timer-task/.timer-elapsed compat stubs left over from an even earlier layout) was fully superseded by the hero card redesign and had zero references left in any .js or .html file — confirmed with a full cross-check, not just the .tb-* prefix grep the backlog note was based on, which also caught .timer-pulse, .timer-paused-lbl, .timer-pause/.timer-stop, and .timer-distract as dead too. Kept .tb-mood-panel/.tb-mood-item (still used by the hero card's own mood picker, interleaved in the middle of the dead block) and @keyframes pulse (still used by .entry.is-timing .edot's animation). _timer.scss goes from 691 to 344 lines; compiled styles.css shrinks by ~4KB gzipped. No behaviour change — verified visually in the browser (mood panel opens with the right styling, .edot's pulse animation still fires) plus the full build/lint/test suite.

Fixed

  • Log view entries sort by start time, not insertion order — viewEntries() (src/js/02-utils.js) produced its newest-first ordering by reversing insertion order, which only matches chronological order when entries are logged in the order they happened. An entry added retroactively — filling in a missed morning slot after the rest of the day was already recorded — therefore landed wherever it happened to be typed rather than at the position its start time called for. It now sorts by ts descending, so an entry always appears where its start time says it belongs regardless of when it was added. Knock-on effect: exportTxt() derives its oldest-first ordering by re-reversing viewEntries(), so the exported timesheet is now genuinely chronological too, rather than reversed-insertion-order. 2 new regression tests cover the ts ordering independent of insertion sequence, and the existing viewed-date filter.
  • Gap check (end-of-week report and end-of-day export warnings) no longer flags non-billable entries for a missing note or proof link — findGapReportEntries() and findExportWarnings() (pure-fns-export.js) previously required every finished, non-cancelled entry to carry a note or link regardless of billing status, so meetings, admin work, and other entries marked internal via the 💰/💸 toggle showed up in the weekly gap report and the export's warnings section even though there's nothing to bill and so nothing to prove. Both functions now skip an entry whose resolved _billable status is false; callers (exportTxt() in 05a-export.js, openGapReportOverlay() in 12c-gapreport.js) resolve that status via the existing isEntryBillable() three-tier lookup before calling in, since the pure module has no access to the category/task state that lookup needs.
  • smoke-tests.cjs couldn't run in sandboxes with a pre-provisioned Chromium that lags Playwright's pinned version — chromium.launch() always resolved the browser Playwright's own installer would have downloaded for the currently pinned playwright version, with no way to point it at a different binary already on disk. In an offline/sandboxed dev environment where a Chromium build exists at an older revision than node_modules/playwright-core/browsers.json expects, and npx playwright install can't reach the network to fetch a matching one, this failed with browserType.launch: Executable doesn't exist at .../chrome-headless-shell. Real CI is unaffected — ci.yml, auto-fix-ci.yml, and auto-chore.yml all run npx playwright install --with-deps chromium before tests, which keeps the download in sync with package.json. chromium.launch() now accepts an optional PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH env var; unset (the default everywhere else), behaviour is unchanged.
  • auto-chore.yml's exit-code checks could misfire on an empty string — the "comment on the issue" step's elif [ "${{ steps.claude.outputs.exit_code }}" != "0" ] and the "fail the job" step's if: steps.claude.outputs.exit_code != '0' both treat an empty/unset exit_code output as "nonzero," since '' != '0' is true. In the current workflow the claude step always sets this output before finishing (wrapped in set +e/set -e), so the gap wasn't reachable in practice — but a hard runner kill mid-step (cancellation, timeout) before that final echo runs would have produced a spurious 🔴 "CLI crashed" comment and an unwarranted red job, misreporting what actually happened. Both checks now require the output to be non-empty first. Flagged as a nitpick on PR #296's own review and tracked as #297; picked up separately since it edits auto-chore.yml itself, which the automation can never push (see the workflow's own header comment).
  • auto-chore.yml failed silently when a push failed, and briefly shipped with an invalid permissions: entry — found by dogfooding the workflow on a real test issue (#297) asking it to modify auto-chore.yml itself: the default GITHUB_TOKEN refused the push ("refusing to allow a GitHub App to create or update workflow ... without workflows permission"), and because that push step failed, the "comment on the issue with the outcome" step was skipped too (GitHub Actions skips subsequent steps by default after a failure) — leaving the issue with zero visibility into what happened. The comment step now runs regardless of an earlier step's outcome (!cancelled()), with a distinct message for "committed but failed to push" versus the existing "declined to automate" and "CLI itself crashed" cases. A same-day follow-up (#299) first tried adding workflows: write to the job's permissions: block — workflows is not a real GitHub Actions token permission scope, and the invalid key made the entire workflow file unparseable, breaking every chore trigger, not just ones touching workflow files. Corrected here: there is no permissions: entry that grants this — pushing changes to .github/workflows/* is a hard limit of the default GITHUB_TOKEN, now documented as a known limitation in the workflow file's header comment. The distinct failure comment above is what actually surfaces this case.
  • auto-chore.yml no longer silently swallows a Claude CLI crash — > chore-output.txt 2>&1 || true discarded the CLI's real exit code, so a crash or auth failure looked identical to Claude correctly declining to act: same green run, same generic issue comment. The step now captures the actual exit code; a nonzero exit posts a distinct 🔴 comment and fails the job outright, while a zero exit with no commit still reports the original "declined to automate" message. Also added a concurrency: auto-chore-<issue-number> group so a fast label-remove-then-reapply can't race two overlapping runs past the existing-branch/PR guard into duplicate PRs, and a ${SLUG:-untitled} fallback for issue titles with no ASCII alphanumeric characters (previously produced a branch name with a trailing dash). Found via /pr-review on #291 after it had already merged.

Added

  • CI now lints the workflow files themselves (actionlint) — an invalid key in a workflow does not fail loudly: GitHub refuses to parse the file, schedules zero jobs, and reports no error anywhere, so the run reads as "nothing to do". That has silently broken this repo twice — PR #256 (if: secrets.CLAUDE_REVIEWER_APP_ID != '', a context unavailable in step-level if:, which left a11y-audit.yml dead for weeks without ever posting a check) and PR #299 (workflows: write in a permissions: block, which killed every chore trigger until #300). Both were found only by checking gh api .../jobs by hand and noticing an empty list. The lint job now runs actionlint, pinned to v1.7.12 and SHA256-verified before it executes. Because a linter that silently stops enforcing is worse than none, npm run test:actionlint also re-runs actionlint against two fixtures reproducing those exact bugs and fails if either rule stops firing — matching on actionlint's message text rather than its rule tag, since tags have been renamed upstream before. Pure parseActionlintFindings(), interpretFixtureRun(), interpretWorkflowsRun() and interpretSelfTest() in .github/scripts/lib/actionlint-selftest.mjs, 20 unit tests that need no binary installed. Linting of run: block contents (shellcheck, pyflakes) is deliberately left off for now — that is a separate, much larger cleanup. Two actions/create-github-app-token input findings are also muted: actionlint v1.7.12 ships a stale schema for that action and reports client-id as undefined and app-id as required, when the action's own action.yml deprecates app-id in favour of client-id — acting on those 18 findings would have moved every workflow onto a deprecated input. The suppression names that one action explicitly, and a unit test asserts no ignore pattern can swallow either fixture's expected finding.
  • The end-of-day export now shows when work happened, not just how long — the plaintext report grouped entries into totals only, so a task worked in two separate sessions (say, morning and afternoon) rendered as one time-blind block, and a proof link recorded on an entry (existing entry.link field) was captured but never actually shown in the file — leaving worklog-vs-report cross-checks to memory. Each task's individual tracked sessions now render as indented HH:MM–HH:MM lines, and its proof link(s) as an indented link: line, same convention as the existing note: line. The header also gains a Breaks (untracked): Xh Ym line (workday span minus tracked time) so a day with no real break stops disappearing into the totals. New sessions field on groupEntriesByCategory()'s per-task output; new pure buildEntryLinkMap().
  • End-of-day export warnings section — a day's totals can look fine while hiding real anomalies (undocumented work, a suspiciously long unbroken stretch, no break in a full day). The export now appends a ⚠ Warnings section flagging entries with neither a note nor a link, single blocks over 4h (reusing the existing isLongRunningTimer() threshold), and a ≥6h day with under 15 minutes of untracked time — so the person logging the day finds the gap before anyone else has to ask about it. Omitted entirely on a clean day. New pure findExportWarnings() in pure-fns-export.js, unit tested.
  • Restarting a timer carries its proof link forward and asks about the note — every "restart with timer" action (the log's ▶ button, the kanban board's "▸ track" button, and the "+ track recent" chips) used to start a completely blank entry, discarding any proof link or note recorded on the previous run of the same task. All three now go through a shared createRestartedEntry() (05-entries.js): the most recent matching entry's proof link is carried over silently, while its note — which may no longer describe what you're about to do — opens the new entry's editor with a "same note as last time?" banner (Yes, keep it / No, clear) instead of being copied automatically. 11 new unit tests cover the lookup, the copy/prompt logic, and the confirmation banner's rendering.
  • Auto-chore GitHub Actions workflow — applying the new chore issue label now triggers auto-chore.yml, which checks out a fresh chore/issue-<N>-<slug> branch, runs Claude against the new /chore-start skill to implement the issue (only when it judges the change small and mechanical — it comments and makes no commit otherwise), verifies with build/lint/test, and — if a commit resulted — pushes the branch and opens a PR that closes the issue. The PR then goes through the existing chatgpt-pr-review / pr-review pipeline like any human-opened PR; nothing merges without that review and explicit approval. A guard skips issues that already have a branch or PR so relabeling doesn't start duplicate runs.
  • CI now guards commitlint against dependency skew — the commit-msg hook is the only thing enforcing Conventional Commits and it runs solely on contributors' machines, so when #251 bumped @commitlint/config-conventional without a matching @commitlint/cli, every local git commit failed with ERR_PACKAGE_PATH_NOT_EXPORTED and nothing noticed for weeks (QA 2026-08-03, priority 3). #290 pinned the transitive dependency; this adds the missing guard. A new npm run test:commitlint runs commitlint against one conforming and one non-conforming sample message, asserting that the preset still loads and that rules are still enforced — a preset that loads but matches nothing would otherwise wave any message through. Wired into the lint job in ci.yml, so the breakage now fails a required check instead of surfacing on someone's next clean checkout. Deliberately does not lint the PR's real commit messages: the point is detecting dependency breakage, not policing contributors. Pure interpretSelfTest(), isPresetResolutionFailure() and selectCommitlintBinPath() in .github/scripts/lib/commitlint-selftest.mjs, 36 unit tests.

Changed

  • End-of-day export's pasteable summary line redesigned around per-ticket totals — previously grouped by category as Category (task, task) with no per-item duration and the Jira key stripped from each task name, which suited a client-facing read but not the line's real job: checking logged hours against ticket worklogs. Now one semicolon-separated Label (duration) item per distinct task, full-day total (a task worked in two sessions still collapses to a single line, matching a Jira worklog), Jira key kept intact, and internal (non-billable) items marked , internal so that split — already in the header totals — is checkable line by line too. Removes buildBillableSummaryParts(), stripJiraPrefix(), and mergeAdjacentEntries() (the last was dead once summary totals moved to whole-day-per-task instead of gap-based adjacency merging, which had a latent bug: an unrelated entry logged between two sessions of the same task broke the adjacency and silently produced two line items instead of one). New pure buildTimesheetSummaryLine() in pure-fns-export.js, unit tested.

Config defaults at this release

These are the compile-time defaults baked into src/js/00-config.js.
Runtime overrides (weather location etc.) are set per-machine in config.local.ps1.

Constant Default value
WEATHER_LAT 60.1887
WEATHER_LON 24.927
WEATHER_NAME 'Helsinki'
JIRA_BASE 'https://your-instance.atlassian.net/browse'
AUTO_PAUSE_ON_TAB_SWITCH false