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
157 changes: 157 additions & 0 deletions .github/workflows/assign-command.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Assign command

on:
issue_comment:
types: [created]

permissions:
issues: write

env:
PROJECT_ID: PVT_kwHOA8O2Dc4BX1OY
STATUS_FIELD_ID: PVTSSF_lAHOA8O2Dc4BX1OYzhS_ZbU
STATUS_IN_PROGRESS: "98236657"
STATUS_BACKLOG: "f75ad846"

jobs:
assign:
if: ${{ !github.event.issue.pull_request && (github.event.comment.body == '/assign' || github.event.comment.body == '/unassign') }}
runs-on: ubuntu-latest
steps:
- name: Handle /assign
id: do_assign
if: github.event.comment.body == '/assign'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const commenter = context.payload.comment.user.login;

const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number });

if (issue.assignees.length > 0) {
const names = issue.assignees.map(a => `@${a.login}`).join(', ');
await github.rest.issues.createComment({
owner, repo, issue_number,
body: `⚠️ This issue is already assigned to ${names}. Ask them to comment \`/unassign\` first if they're no longer working on it.`,
});
core.setOutput('assigned', 'false');
return;
}

await github.rest.issues.addAssignees({ owner, repo, issue_number, assignees: [commenter] });
await github.rest.issues.createComment({
owner, repo, issue_number,
body: `✅ Assigned to @${commenter}. Comment \`/unassign\` if you can no longer work on this. Open a PR that includes \`Closes #${issue_number}\` in its description when you're ready for review.`,
});
core.setOutput('assigned', 'true');

- name: Move to In progress
if: github.event.comment.body == '/assign' && steps.do_assign.outputs.assigned == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_TOKEN }}
script: |
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
});

const { node } = await github.graphql(
`query($issueId: ID!) {
node(id: $issueId) {
... on Issue { projectItems(first: 10) { nodes { id project { id } } } }
}
}`,
{ issueId: issue.node_id }
);

const item = node.projectItems.nodes.find(n => n.project.id === process.env.PROJECT_ID);
if (!item) {
console.log('Issue is not on the project board; skipping status update.');
return;
}

await github.graphql(
`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId, itemId: $itemId, fieldId: $fieldId,
value: { singleSelectOptionId: $optionId }
}) { projectV2Item { id } }
}`,
{
projectId: process.env.PROJECT_ID,
itemId: item.id,
fieldId: process.env.STATUS_FIELD_ID,
optionId: process.env.STATUS_IN_PROGRESS,
}
);

- name: Handle /unassign
id: do_unassign
if: github.event.comment.body == '/unassign'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const commenter = context.payload.comment.user.login;

const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number });
const isAssigned = issue.assignees.some(a => a.login === commenter);

if (!isAssigned) {
await github.rest.issues.createComment({
owner, repo, issue_number,
body: `⚠️ @${commenter}, you're not currently assigned to this issue.`,
});
core.setOutput('unassigned', 'false');
return;
}

await github.rest.issues.removeAssignees({ owner, repo, issue_number, assignees: [commenter] });
await github.rest.issues.createComment({
owner, repo, issue_number,
body: `Unassigned @${commenter}. This issue is open again — comment \`/assign\` to pick it up.`,
});
core.setOutput('unassigned', 'true');

- name: Move to Backlog
if: github.event.comment.body == '/unassign' && steps.do_unassign.outputs.unassigned == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_TOKEN }}
script: |
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
});

const { node } = await github.graphql(
`query($issueId: ID!) {
node(id: $issueId) {
... on Issue { projectItems(first: 10) { nodes { id project { id } } } }
}
}`,
{ issueId: issue.node_id }
);

const item = node.projectItems.nodes.find(n => n.project.id === process.env.PROJECT_ID);
if (!item) {
console.log('Issue is not on the project board; skipping status update.');
return;
}

await github.graphql(
`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId, itemId: $itemId, fieldId: $fieldId,
value: { singleSelectOptionId: $optionId }
}) { projectV2Item { id } }
}`,
{
projectId: process.env.PROJECT_ID,
itemId: item.id,
fieldId: process.env.STATUS_FIELD_ID,
optionId: process.env.STATUS_BACKLOG,
}
);
76 changes: 76 additions & 0 deletions .github/workflows/pr-board-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: PR board sync

on:
pull_request:
types: [opened, edited, ready_for_review]

permissions:
contents: read

env:
PROJECT_ID: PVT_kwHOA8O2Dc4BX1OY
STATUS_FIELD_ID: PVTSSF_lAHOA8O2Dc4BX1OYzhS_ZbU
STATUS_READY_TO_REVIEW: c6aa22db

jobs:
move-linked-issues:
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- name: Move linked issues to Ready to review
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_TOKEN }}
script: |
const body = context.payload.pull_request.body || '';
const keywords = '(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)';
const re = new RegExp(`${keywords}\\s+#(\\d+)`, 'gi');
const issueNumbers = [...new Set([...body.matchAll(re)].map(m => Number(m[1])))];

if (issueNumbers.length === 0) {
console.log('No closing keyword found in the PR description; nothing to move.');
return;
}

for (const issue_number of issueNumbers) {
let issue;
try {
({ data: issue } = await github.rest.issues.get({
owner: context.repo.owner, repo: context.repo.repo, issue_number,
}));
} catch (err) {
console.log(`Issue #${issue_number} not found in this repo, skipping.`);
continue;
}

const { node } = await github.graphql(
`query($issueId: ID!) {
node(id: $issueId) {
... on Issue { projectItems(first: 10) { nodes { id project { id } } } }
}
}`,
{ issueId: issue.node_id }
);

const item = node.projectItems.nodes.find(n => n.project.id === process.env.PROJECT_ID);
if (!item) {
console.log(`Issue #${issue_number} is not on the project board, skipping.`);
continue;
}

await github.graphql(
`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId, itemId: $itemId, fieldId: $fieldId,
value: { singleSelectOptionId: $optionId }
}) { projectV2Item { id } }
}`,
{
projectId: process.env.PROJECT_ID,
itemId: item.id,
fieldId: process.env.STATUS_FIELD_ID,
optionId: process.env.STATUS_READY_TO_REVIEW,
}
);
console.log(`Moved issue #${issue_number} to Ready to review.`);
}
48 changes: 48 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Contributing to Modly

Thanks for wanting to help out! You don't need write access to the repository to
pick up a ticket, work on it, and ship a fix — here's how the flow works.

## Finding something to work on

- Browse [open issues](https://github.com/lightningpixel/modly/issues) or the
[project board](https://github.com/users/lightningpixel/projects/1).
- Issues labeled `good first issue` are a good place to start if you're new to
the codebase. See [`CLAUDE.md`](./CLAUDE.md) for an architecture overview.

## Claiming a ticket

Comment **`/assign`** on the issue you want to work on. A bot will assign it to
you automatically — no repo permissions required.

- Only one person can be assigned to an issue at a time. If it's already
assigned, ask the assignee first or wait for them to release it.
- No longer working on it? Comment **`/unassign`** to free it up for someone
else.

This keeps the [project board](https://github.com/users/lightningpixel/projects/1)
honest: an assigned issue moves to **In progress** automatically, so anyone
looking at the board can see what's actively being worked on.

## Submitting your work

1. **Fork** the repository and create a branch for your change.
2. Make your change. Keep it focused — one issue, one PR.
3. Run the checks locally before opening a PR:
```bash
npm run lint
npm run test
```
4. Open a **pull request** against `dev`. Include `Closes #<issue-number>` in
the PR description so it's linked to the ticket and closes it automatically
on merge.

Opening a PR from your fork moves the linked issue to **Ready to review** on
the board. Once a maintainer approves the review, it moves to **Ready to
test**; once merged, it moves to **Done**.

## Getting help

If something in an issue is unclear, ask in a comment on the issue itself
before starting — it's cheaper to clarify scope up front than to redo work
later.
2 changes: 1 addition & 1 deletion api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def filter(self, record):

app = FastAPI(
title="Modly API",
version="0.4.1",
version="0.4.2",
lifespan=lifespan,
)

Expand Down
Loading