Skip to content
Open
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
2 changes: 1 addition & 1 deletion .dependabot/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ update_configs:
update_schedule: "live"
allowed_updates:
- match:
update_type: "security"
update_type: "security"
22 changes: 11 additions & 11 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ name: "CodeQL"

on:
push:
branches: [ main ]
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '28 7 * * 0'
- cron: "28 7 * * 0"

permissions:
contents: read
Expand All @@ -19,13 +19,13 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v2

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
queries: security-and-quality
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
queries: security-and-quality

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
54 changes: 27 additions & 27 deletions InclusionBot.md

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions manifest.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
applications:
- name: charlie
buildpack: nodejs_buildpack
memory: 192MB
instances: 1
command: ((command))
routes:
- route: charlie-((environment)).app.cloud.gov
services:
- charlie-brain
- charlie-config
- name: charlie
buildpack: nodejs_buildpack
memory: 192MB
instances: 1
command: ((command))
routes:
- route: charlie-((environment)).app.cloud.gov
services:
- charlie-brain
- charlie-config
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"js-yaml": "^4.2.0",
"moment": "^2.29.2",
"moment-timezone": "^0.5.35",
"node-cron": "^4.5.0",
"node-schedule": "^2.0.0",
"pg": "^8.7.1",
"plural": "^1.1.0"
Expand Down
4 changes: 2 additions & 2 deletions src/scripts/erg-inviter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ ergs:
career development, address the challenges we encounter in the workplace,
and help one another in and around TTS.
channel: "#womxn"

Not Your Dude:
description: >
Private channel for cis women, trans women, trans men, non-binary people,
and those who are otherwise marginalized (i.e., non-hegemonic masculine
folks).
channel: "#womxn-private"
channel: "#womxn-private"
9 changes: 4 additions & 5 deletions src/scripts/inclusion-bot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ message: >
are some alternatives that might work better:

triggers:

- matches:
- blind to
- blinded to
Expand Down Expand Up @@ -170,7 +169,7 @@ triggers:
- matches:
- fuck
alternatives:
- ''
- ""
why: >-
":TERM:" can trigger emotions related to abuse, trauma, and sexual assault
that employees may not want to process at work.
Expand Down Expand Up @@ -416,7 +415,7 @@ triggers:
- expert
- whiz
ignore:
- ':ninja:'
- ":ninja:"
- ninja turtle
- ninja turtles
- ninja-turtle
Expand Down Expand Up @@ -557,15 +556,15 @@ triggers:
- suicidal
- kill yourself
alternatives:
- ''
- ""
why: >-
":TERM:" can trigger emotions related to death that employees may not want
to process at work.

- matches:
- shit
alternatives:
- ''
- ""
why: >-
":TERM:" can trigger emotions related to vulgarity that employees may not
want to process at work.
Expand Down
174 changes: 174 additions & 0 deletions src/track-meet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Track Meet Facilitator Bot
* Posts a message to the shared channel every 2nd/4th Friday at 11am PT
* listing facilitators for the upcoming Wednesday track meets.
* Reads facilitator names from each track's Slack Canvas table.
*/

const cron = require("node-cron");

/**
* SHARED_CHANNEL_ID is the #usdc-all-fellows channel where the bot posts the message.
*/
const SHARED_CHANNEL_ID = "C05A6Q2PK6G";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion (non-blocking)] For ease of later debugging, a comment here that includes the channel name and/or a link to open the channel could be useful

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] has charlie been invited into this channel? It looks like it's probably a private channel, so if charlie isn't in it then this script likely won't work


/**
* TRACK_CANVASES is an object mapping track names to their respective Slack Canvas IDs.
*/
const TRACK_CANVASES = {
Cyber: "F091D6QD9RN",
Data: "F091D7L8M5E",
Design: "F090GG1L478",
Product: "F090QH1MQMQ",
Software: "F0911VCAJTB",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] are all of these canvases shared with the SHARED_CHANNEL_ID? If not, then you might need to double-check if charlie has access to them


// ── DATE LOGIC ────────────────────────────────────────────────────────────────

function getWednesdayNumber(d) {
return Math.floor((d.getDate() - 1) / 7) + 1;
}

const NEW_SCHEDULE_ANCHOR = new Date("2026-07-15"); // First track meet under new every-other-Wednesday rule

function isTrackMeetWeek(d) {
if (d < NEW_SCHEDULE_ANCHOR) {
// Old rule: 2nd and 4th Wednesdays only
const n = getWednesdayNumber(d);
return n === 2 || n === 4;
}
// New rule: every other Wednesday anchored to 7/15/26
const msPerWeek = 7 * 24 * 60 * 60 * 1000;
const weeksDiff = Math.round((d - NEW_SCHEDULE_ANCHOR) / msPerWeek);
return weeksDiff % 2 === 0;
}

function getNextNextWednesday() {
// From Friday, skip the immediate Wednesday (5 days away)
// and get the one after that (12 days away)
const d = new Date();
const daysAhead = (3 - d.getDay() + 7) % 7 || 7; // days until next Wednesday
d.setDate(d.getDate() + daysAhead + 7); // add another 7 to get the one after
return d;
}

function getFridayBefore(wed) {
const d = new Date(wed);
d.setDate(d.getDate() - 5);
return d.toLocaleDateString("en-US", { month: "long", day: "numeric" });
}

function formatDate(d) {
return d.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}

// ── CANVAS READING ────────────────────────────────────────────────────────────

async function getFacilitatorForDate(client, canvasId, targetDate) {
const targetStrs = [
targetDate.toLocaleDateString("en-US", { month: "numeric", day: "numeric", year: "2-digit" }),
targetDate.toLocaleDateString("en-US", { month: "numeric", day: "numeric", year: "numeric" }),
targetDate.toLocaleDateString("en-US", { month: "long", day: "numeric" }),
];

try {
const res = await client.canvases.sections.lookup({
canvas_id: canvasId,
criteria: { section_types: ["any_ordered_list", "table"] },
});

for (const section of res.sections || []) {
const content = section.content || "";
for (const fmt of targetStrs) {
if (content.includes(fmt)) {
for (const line of content.split("\n")) {
if (line.includes(fmt)) {
const parts = line.split("|").map((p) => p.trim()).filter(Boolean);
if (parts.length >= 2) return parts[1];
}
}
}
}
}
return "TBD";
} catch (err) {
console.error(`Error reading canvas ${canvasId}:`, err.message);
return "TBD";
}
}

// ── MESSAGE BUILDER ───────────────────────────────────────────────────────────

function buildMessage(facilitators, facilitationDate) {
const dateStr = formatDate(facilitationDate);
const fridayStr = getFridayBefore(facilitationDate);
const bullets = Object.entries(facilitators)
.map(([track, name]) => `• ${track}: ${name}`)
.join("\n");

return `Track Meet facilitators on deck for ${dateStr}. Facilitators, please coordinate directly with any presenters and let the track channel know if there is anything they will need to prepare. Also, please share the agenda in your track channel (and in this thread, noting if the meet is open to other tracks!) by Noon ET on ${fridayStr}. The facilitators for next week's track meets are below:

${bullets}

To organize a cross-track meet, reach out to the facilitator for that track. Track facilitation schedules are also pinned in the respective track Slack channels. As a reminder, the track facilitation schedules can be found in the "Meet Facilitation Schedules" folder in this channel's tabs, above.

If you have a conflict (planned leave, agency conflict, etc) with your date to facilitate your track meet, it is your responsibility to coordinate a backup to cover facilitation.`;
}

// ── MAIN JOB ──────────────────────────────────────────────────────────────────

async function runBot(client) {
const targetWed = getNextNextWednesday();
console.log(`Track meet bot running. Target Wednesday: ${formatDate(targetWed)}`);

if (!isTrackMeetWeek(targetWed)) {
console.log(
`Skipping — ${getWednesdayNumber(targetWed)} Wednesday of the month (all-hands or wild card).`
);
return;
}

const trackEntries = Object.entries(TRACK_CANVASES);
const facilitatorList = await Promise.all(
trackEntries.map(([, canvasId]) => getFacilitatorForDate(client, canvasId, targetWed))
);

const facilitators = {};
trackEntries.forEach(([track], i) => {
facilitators[track] = facilitatorList[i];
console.log(` ${track}: ${facilitators[track]}`);
});

// Only skip if ALL tracks are TBD — a single missing facilitator still posts
const allTBD = Object.values(facilitators).every((name) => name === "TBD");
if (allTBD) {
console.log("No facilitators found in any canvas — skipping post.");
return;
}

const message = buildMessage(facilitators, targetWed);

try {
await client.chat.postMessage({ channel: SHARED_CHANNEL_ID, text: message });
console.log("Track meet message posted successfully.");
} catch (err) {
console.error("Error posting message:", err.message);
}
}

// ── CHARLIE ENTRY POINT ───────────────────────────────────────────────────────

module.exports = (app) => {
// Runs every Friday at 11:00 AM PT (19:00 UTC during daylight saving time)
cron.schedule(
"0 19 * * 5",
() => { runBot(app.client); },
{ timezone: "America/Los_Angeles" }
);

console.log("Track meet bot loaded. Scheduled for Fridays at 11:00 AM PT.");
};
Loading