Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/skills/bmad-retrospective/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ Alice (Product Owner): "Good thinking - helps us connect what we learned to what
- Deployment or environment setup

<output>

Bob (Scrum Master): "Alright, I've reviewed Epic {{next_epic_num}}: '{{next_epic_title}}'"

Alice (Product Owner): "What are we looking at?"
Expand Down
1 change: 1 addition & 0 deletions .github/skills/bmad-sprint-status/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat

<check if="any status is unrecognized">
<output>

⚠️ **Unknown status detected:**
{{#each invalid_entries}}

Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: 'CodeQL'

permissions: {}

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 17 * * 5' # Weekly scan (Friday 12:00 PM EST / 17:00 UTC)

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: ['javascript-typescript', 'actions']

steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Initialize CodeQL
uses: github/codeql-action/init@bb16b9baa2ec4010b29f5c606d57d01190139edd # v4.37.1
with:
languages: ${{ matrix.language }}

- name: Autobuild
uses: github/codeql-action/autobuild@bb16b9baa2ec4010b29f5c606d57d01190139edd # v4.37.1

- name: Run CodeQL analysis
uses: github/codeql-action/analyze@bb16b9baa2ec4010b29f5c606d57d01190139edd # v4.37.1
with:
category: '/language:${{ matrix.language }}'
14 changes: 11 additions & 3 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ jobs:
permissions:
contents: read
pull-requests: read
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '20'
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Generate test coverage
run: npm test -- --coverage
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: SonarCloud Scan
if: ${{ env.SONAR_TOKEN != '' }}
if: ${{ secrets.SONAR_TOKEN != '' }}
uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ Alice (Product Owner): "Good thinking - helps us connect what we learned to what
- Deployment or environment setup

<output>

Bob (Scrum Master): "Alright, I've reviewed Epic {{next_epic_num}}: '{{next_epic_title}}'"

Alice (Product Owner): "What are we looking at?"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat

<check if="any status is unrecognized">
<output>

⚠️ **Unknown status detected:**
{{#each invalid_entries}}

Expand Down
36 changes: 19 additions & 17 deletions deploy/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,6 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
} catch {
// best-effort; ignore storage errors
}
const safeId = escapeHtml(scriptId)
const section = document.getElementById('setup-section-' + scriptId)
if (section) {
section.innerHTML = `<div style="font-size:12px;color:#137333;">⏱ Trigger previously activated.</div>`
Expand All @@ -667,7 +666,7 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
* to activate the hourly trigger.
*/
function buildSetupSource(triggerFn) {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(triggerFn)) {
if (!/^[a-zA-Z_]\w*$/.test(triggerFn)) {
throw new Error(
'Invalid trigger function name: ' + JSON.stringify(triggerFn)
)
Expand Down Expand Up @@ -718,7 +717,7 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
)
const anyNeedsUpdate = checked.some((el) => {
const v = scriptVersions.get(el.value)
return v && v.needsUpdate
return v?.needsUpdate
})
if (anyNeedsUpdate) {
btn.textContent = 'Update & Deploy to my account'
Expand Down Expand Up @@ -1565,11 +1564,11 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
fields.email ? [fields.email] : []
)
const subject = JSON.stringify(fields.subject || 'Weekly Briefing')
const lookahead = parseInt(fields.lookaheadDays, 10) || 7
const lookahead = Number.parseInt(fields.lookaheadDays, 10) || 7
const freq = JSON.stringify(fields.frequency || 'weekly')
const day = JSON.stringify(fields.triggerDay || 'MONDAY')
const hour = parseInt(fields.triggerHour, 10) || 7
const interval = parseInt(fields.triggerInterval, 10) || 1
const hour = Number.parseInt(fields.triggerHour, 10) || 7
const interval = Number.parseInt(fields.triggerInterval, 10) || 1
let scheduleLines =
' scheduleFrequency: ' +
freq +
Expand Down Expand Up @@ -1674,12 +1673,12 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
}

function escapeHtml(str) {
return String(str == null ? '' : str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
return String(str ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
}

/**
Expand All @@ -1688,7 +1687,7 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
* looks tampered-with so it can never be injected into HTML or URLs.
*/
function validateScriptId(id) {
if (typeof id !== 'string' || !/^[A-Za-z0-9_-]+$/.test(id)) {
if (typeof id !== 'string' || !/^[\w-]+$/.test(id)) {
throw new Error(`Invalid script ID: ${String(id).slice(0, 64)}`)
}
return id
Expand Down Expand Up @@ -1977,7 +1976,11 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>

showStatus(
'ok',
`✅ <strong>${deployedProjects.length === 1 ? 'Script deployed!' : `${deployedProjects.length} scripts deployed!`}</strong>` +
`✅ <strong>${
deployedProjects.length === 1
? 'Script deployed!'
: `${deployedProjects.length} scripts deployed!`
}</strong>` +
`<span style="font-size:13px;color:#137333;display:block;margin-top:4px;">` +
`Now configure each script below, then activate the trigger.</span>` +
projectLinksHtml
Expand All @@ -1986,8 +1989,7 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
// Show Step 3 config cards
renderConfigCards(deployedProjects)
} catch (err) {
const isApiDisabled =
err.message && err.message.includes('Apps Script API')
const isApiDisabled = err.message?.includes('Apps Script API')
const errorDetail = err.stack || err.message || String(err)
const errorDetailBlock =
`<details style="margin-top:6px;font-size:12px;">` +
Expand Down Expand Up @@ -2056,7 +2058,7 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
let detail = body
try {
const json = JSON.parse(body)
if (json.error && json.error.message) {
if (json.error?.message) {
message = json.error.message
detail = json.error.details
? `${json.error.message} — ${JSON.stringify(json.error.details)}`
Expand Down
7 changes: 4 additions & 3 deletions deploy/tests/ui.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/

const { test, expect } = require('@playwright/test')
const path = require('path')
const path = require('node:path')

const PAGE_URL = `file://${path.resolve(__dirname, '..', 'index.html')}`

Expand Down Expand Up @@ -273,8 +273,9 @@ test.describe('deploy index.html', () => {
page,
}) => {
await signIn(page)
// Default mock returns empty files array
await page.waitForTimeout(500)
// Default mock returns empty files array; wait for sign-in to settle by
// observing Step 2 render, then assert Step 3 never appears.
await expect(page.locator('#step2-card')).toBeVisible()
await expect(page.locator('#step3-card')).toBeHidden()
})

Expand Down
8 changes: 4 additions & 4 deletions gas-installer/Index.html
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,10 @@ <h2><span class="step-badge">2</span>Name your project</h2>

function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
}
</script>
</body>
Expand Down
7 changes: 3 additions & 4 deletions gas-installer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const APPS_SCRIPT_API_BASE = 'https://script.googleapis.com/v1'
*/
function getFileType(filename) {
if (!filename || typeof filename !== 'string') return null
if (/\.gs$/.test(filename)) return 'SERVER_JS'
if (/\.html$/.test(filename)) return 'HTML'
if (filename.endsWith('.gs')) return 'SERVER_JS'
if (filename.endsWith('.html')) return 'HTML'
return null
}

Expand All @@ -42,8 +42,7 @@ function filterGithubItems(items) {
return items
.filter(
(item) =>
item &&
item.type === 'file' &&
item?.type === 'file' &&
typeof item.name === 'string' &&
item.download_url
)
Expand Down
2 changes: 1 addition & 1 deletion gas-installer/tests/ui.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

const { test, expect } = require('@playwright/test')
const path = require('path')
const path = require('node:path')

const PAGE_URL = `file://${path.resolve(__dirname, '..', 'Index.html')}`

Expand Down
8 changes: 4 additions & 4 deletions scripts/check-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* its minimum threshold, or 0 if all pass.
*/

const fs = require('fs')
const path = require('path')
const fs = require('node:fs')
const path = require('node:path')

const coveragePath = path.join(process.cwd(), 'coverage', 'coverage-final.json')

Expand Down Expand Up @@ -44,9 +44,9 @@ for (const filePath in coverage) {
if (fileCoverage.b) {
for (const key in fileCoverage.b) {
const branches = fileCoverage.b[key]
for (let i = 0; i < branches.length; i++) {
for (const branch of branches) {
totals.branches.total++
if (branches[i] > 0) totals.branches.covered++
if (branch > 0) totals.branches.covered++
}
}
}
Expand Down
1 change: 1 addition & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ sonar.organization=petry-projects
sonar.projectName=google-app-scripts
sonar.sources=.
sonar.exclusions=_bmad/**,_bmad-output/**,.claude/**
sonar.javascript.lcov.reportPaths=coverage/lcov.info

# SonarCloud S7637 exemption for first-party reusable-ref caller stubs (#404).
# These thin caller stubs contain ONLY a petry-projects/.github(-private)
Expand Down
6 changes: 3 additions & 3 deletions src/calendar-to-briefing-doc/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function formatConflictWarning(conflicts, formatTime) {
function groupEventsByDay(events, getDateKey) {
const map = new Map()
for (const item of events) {
const evt = item && item.event ? item.event : item
const evt = item?.event ? item.event : item
const key = getDateKey(evt.getStartTime())
if (!map.has(key)) map.set(key, [])
map.get(key).push(item)
Expand Down Expand Up @@ -213,7 +213,7 @@ function formatBriefing(title, groupedEvents, formatTime) {
sections.push(dayLabel)

// Detect if items are tuples or plain events
const isTuple = items.length > 0 && items[0] && items[0].event
const isTuple = items.length > 0 && items[0]?.event
const tuples = isTuple
? items
: items.map((e) => ({ event: e, calendarName: null }))
Expand Down Expand Up @@ -266,7 +266,7 @@ function emailBriefing(gmailApp, recipients, subject, body) {
*/
function shouldRunNow(config, now, lastRunMs) {
const hour = now.getHours()
const scheduleHour = config.scheduleHour != null ? config.scheduleHour : 7
const scheduleHour = config.scheduleHour ?? 7

if (hour !== scheduleHour) return false

Expand Down
16 changes: 6 additions & 10 deletions src/calendar-to-briefing-doc/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,12 @@ describe('groupEventsByDay', () => {
// ── formatDayLabel ────────────────────────────────────────────────────────────

describe('formatDayLabel', () => {
it('formats a Monday correctly', () => {
expect(formatDayLabel('2025-01-13')).toBe('Monday, January 13')
})

it('formats a Saturday in December correctly', () => {
expect(formatDayLabel('2025-12-27')).toBe('Saturday, December 27')
})

it('formats a Sunday in February correctly', () => {
expect(formatDayLabel('2025-02-02')).toBe('Sunday, February 2')
it.each([
['a Monday', '2025-01-13', 'Monday, January 13'],
['a Saturday in December', '2025-12-27', 'Saturday, December 27'],
['a Sunday in February', '2025-02-02', 'Sunday, February 2'],
])('formats %s correctly', (_desc, input, expected) => {
expect(formatDayLabel(input)).toBe(expected)
})
})

Expand Down
3 changes: 2 additions & 1 deletion src/calendar-to-sheets/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ async function syncCalendarToSheet(
}
// delete from bottom to top
console.log('[syncCalendarToSheet] Deleting rows:', toDelete.length)
toDelete.sort((a, b) => b - a).forEach((r) => sheet.deleteRow(r))
toDelete.sort((a, b) => b - a)
toDelete.forEach((r) => sheet.deleteRow(r))
console.log('[syncCalendarToSheet] Sync complete')
}

Expand Down
10 changes: 5 additions & 5 deletions src/deploy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ function buildProjectContent(files) {
}
for (const f of files) {
if (!f || typeof f.name !== 'string' || !f.name) {
throw new Error('Each file must have a non-empty string name')
throw new TypeError('Each file must have a non-empty string name')
}
if (typeof f.source !== 'string') {
throw new Error('Each file must have a string source')
throw new TypeError('Each file must have a string source')
}
}
return {
Expand All @@ -124,7 +124,7 @@ function buildProjectContent(files) {
*/
async function createProject(fetchFn, accessToken, title) {
if (typeof fetchFn !== 'function')
throw new Error('fetchFn must be a function')
throw new TypeError('fetchFn must be a function')
if (!accessToken) throw new Error('accessToken is required')
if (!title) throw new Error('title is required')

Expand Down Expand Up @@ -159,7 +159,7 @@ async function createProject(fetchFn, accessToken, title) {
*/
async function updateProjectContent(fetchFn, accessToken, scriptId, content) {
if (typeof fetchFn !== 'function')
throw new Error('fetchFn must be a function')
throw new TypeError('fetchFn must be a function')
if (!accessToken) throw new Error('accessToken is required')
if (!scriptId) throw new Error('scriptId is required')
if (!content || !Array.isArray(content.files)) {
Expand Down Expand Up @@ -227,7 +227,7 @@ const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1'
*/
async function createGmailLabel(fetchFn, accessToken, labelName) {
if (typeof fetchFn !== 'function')
throw new Error('fetchFn must be a function')
throw new TypeError('fetchFn must be a function')
if (!accessToken) throw new Error('accessToken is required')
if (!labelName) throw new Error('labelName is required')

Expand Down
Loading