Skip to content
Merged
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
414 changes: 240 additions & 174 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions packages/app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ All notable changes to [wuffle](https://github.com/nikku/wuffle) are documented

_**Note:** Yet to be released changes appear here._

## 0.76.0

* `FEAT`: support GitHub sub-issues ([#313](https://github.com/nikku/wuffle/pull/313))
* `FEAT`: validate GitHub application on startup ([#313](https://github.com/nikku/wuffle/pull/313))
* `DEPS`: update to `body-parser@2.3.0`

### Breaking Changes

* Application requires `sub_issues` events for full synchronization to work

## 0.75.0

* `FEAT`: synchronize standard fields for comment authors
Expand Down
1 change: 1 addition & 0 deletions packages/app/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ default_events:
# - team
# - team_add
# - watch
- sub_issues

# The set of permissions needed by the GitHub App. The format of the object uses
# the permission name for the key (for example, issues) and the access type for
Expand Down
23 changes: 20 additions & 3 deletions packages/app/lib/apps/events-sync/EventsSync.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,29 @@ export default function EventsSync(webhookEvents, store, logger) {
});


// issues ///////////////////////////////
// sub-issues /////////////////////

// https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads#issues

// issue transfer is mapped to the following GitHub events
// https://docs.github.com/en/webhooks/webhook-events-and-payloads#sub_issues
//
// update sub-issues on change - updating parent <> child relationship
webhookEvents.on(
/** @type {any} */ ([ 'sub_issues.sub_issue_added', 'sub_issues.sub_issue_removed' ]),
async ({ payload }) => {

const {
sub_issue,
repository
} = /** @type {any} */ (payload);

return store.updateIssue(filterIssue(sub_issue, repository));
}
);


// https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads#issues
//
// issue transfer is mapped to the following GitHub events:
// -> issues.opened (new issue is being opened by GitHub)
// -> issues.transferred (old issue was deleted by GitHub)
//
Expand Down
32 changes: 30 additions & 2 deletions packages/app/lib/apps/github-app/GithubApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ const RequiredEvents = [
'issues',
'issue_comment',
'label',
'member',
'milestone',
'pull_request',
'pull_request_review',
'repository',
'status'
'status',
'sub_issues'
];

/**
Expand All @@ -41,8 +43,9 @@ const RequiredEvents = [
* @param {import('../../types.js').ProbotApp} app
* @param {import('../../types.js').Logger} logger
* @param {import('../../types.js').Injector} injector
* @param {import('../../events.js').default} events
*/
export default function GithubApp(config, app, logger, injector) {
export default function GithubApp(config, app, logger, injector, events) {

const log = logger.child({
name: 'wuffle:github-app'
Expand Down Expand Up @@ -266,6 +269,31 @@ export default function GithubApp(config, app, logger, injector) {
log.debug('validated installations');
}

async function validateApp() {
const octokit = await getAppScopedClient();
const { data: app } = await octokit.rest.apps.getAuthenticated();

// app may not be configured yet
if (!app) {
return;
}

const missingEvents = RequiredEvents.filter(
event => !app.events.includes(event)
);

if (missingEvents.length) {
log.error({
missingEvents,
events: app.events
}, 'app is missing required event subscriptions; update app settings on GitHub');
}
}

events.once('wuffle.start', async function() {
await validateApp().catch(err => log.warn({ err }, 'failed to validate app configuration'));
});

/**
* Fetch active installations.
*
Expand Down
6 changes: 4 additions & 2 deletions packages/app/lib/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ export function filterIssue(githubIssue, githubRepository) {
milestone,
pull_request,
html_url,
author_association
author_association,
parent_issue_url
} = githubIssue;

// stable ID that is independent from GitHubs internal issue/pr distinction
Expand Down Expand Up @@ -245,7 +246,8 @@ export function filterIssue(githubIssue, githubRepository) {
repository: filterRepository(githubRepository),
pull_request: !!pull_request,
html_url,
author_association
author_association,
parent_issue_url: parent_issue_url || null
};

}
16 changes: 16 additions & 0 deletions packages/app/lib/util/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ export function findLinks(issue, types) {
links.push(link);
}

// add CHILD_OF link from parent_issue_url (GitHub sub-issues)
const { parent_issue_url } = issue;

if (parent_issue_url) {
const parentMatch = parent_issue_url.match(/\/repos\/([^/]+)\/([^/]+)\/issues\/(\d+)/);

if (parentMatch) {
links.push({
type: CHILD_OF,
owner: parentMatch[1],
repo: parentMatch[2],
number: parseInt(parentMatch[3], 10)
});
}
}

if (typeof types !== 'undefined') {
return filterLinks(links, types);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.1037.0",
"async-didi": "^1.0.0",
"body-parser": "^2.2.2",
"body-parser": "^2.3.0",
"compression": "^1.8.1",
"express-session": "^1.19.0",
"fake-tag": "^5.0.0",
Expand All @@ -64,10 +64,10 @@
"@types/sinon": "^21.0.1",
"chai": "^6.2.2",
"graphql": "^17.0.0",
"mocha": "^11.7.5",
"nock": "^14.0.13",
"mocha": "^11.7.6",
"nock": "^14.0.15",
"nodemon": "^3.1.14",
"npm-run-all2": "^9.0.0",
"npm-run-all2": "^9.0.2",
"sinon": "^22.0.0",
"sinon-chai": "^4.0.0",
"typescript": "^5.9.3"
Expand Down
42 changes: 42 additions & 0 deletions packages/app/test/apps/events-sync/EventsSync.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,48 @@ describe('apps/events-sync', function() {
});
});


it('sub_issues.sub_issue_added', async function() {

// when
await webhookEvents.emit(
event('16-sub_issues.sub_issue_added')
);

// then
expectIssue(store, {
key: 'nikku/testtest#131',
number: 131,
title: 'Sub issue',
repository: {
name: 'testtest'
},
pull_request: false,
state: 'open',
parent_issue_url: 'https://api.github.com/repos/nikku/testtest/issues/130'
});
});


it('sub_issues.sub_issue_removed', async function() {

// given
await webhookEvents.emit(
event('16-sub_issues.sub_issue_added')
);

// when
await webhookEvents.emit(
event('17-sub_issues.sub_issue_removed')
);

// then
expectIssue(store, {
key: 'nikku/testtest#131',
parent_issue_url: null
});
});

});

});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"name": "sub_issues",
"payload": {
"action": "sub_issue_added",
"parent_issue_id": 4411391091,
"parent_issue": {
"url": "https://api.github.com/repos/nikku/testtest/issues/130",
"id": 4411391091,
"number": 130,
"title": "New issue",
"state": "open"
},
"parent_issue_repo": {
"id": 150751504,
"name": "testtest",
"owner": {
"login": "nikku"
}
},
"sub_issue_id": 4411391092,
"sub_issue": {
"url": "https://api.github.com/repos/nikku/testtest/issues/131",
"repository_url": "https://api.github.com/repos/nikku/testtest",
"html_url": "https://github.com/nikku/testtest/issues/131",
"id": 4411391092,
"node_id": "I_kwDOCPxJEM8AAAABBvB8dA",
"number": 131,
"title": "Sub issue",
"user": {
"login": "nikku",
"id": 58601,
"node_id": "MDQ6VXNlcjU4NjAx",
"avatar_url": "https://avatars.githubusercontent.com/u/58601?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/nikku",
"html_url": "https://github.com/nikku",
"type": "User",
"site_admin": false
},
"labels": [],
"state": "open",
"locked": false,
"assignees": [],
"milestone": null,
"comments": 0,
"created_at": "2026-05-09T06:00:00Z",
"updated_at": "2026-05-09T06:00:00Z",
"closed_at": null,
"assignee": null,
"author_association": "OWNER",
"active_lock_reason": null,
"body": null,
"state_reason": null,
"parent_issue_url": "https://api.github.com/repos/nikku/testtest/issues/130"
},
"repository": {
"id": 150751504,
"node_id": "MDEwOlJlcG9zaXRvcnkxNTA3NTE1MDQ=",
"name": "testtest",
"full_name": "nikku/testtest",
"private": false,
"owner": {
"login": "nikku",
"id": 58601,
"node_id": "MDQ6VXNlcjU4NjAx",
"avatar_url": "https://avatars.githubusercontent.com/u/58601?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/nikku",
"html_url": "https://github.com/nikku",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/nikku/testtest"
},
"installation": {
"id": 48472471,
"node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNDg0NzI0NzE="
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"name": "sub_issues",
"payload": {
"action": "sub_issue_removed",
"parent_issue_id": 4411391091,
"parent_issue": {
"url": "https://api.github.com/repos/nikku/testtest/issues/130",
"id": 4411391091,
"number": 130,
"title": "New issue",
"state": "open"
},
"parent_issue_repo": {
"id": 150751504,
"name": "testtest",
"owner": {
"login": "nikku"
}
},
"sub_issue_id": 4411391092,
"sub_issue": {
"url": "https://api.github.com/repos/nikku/testtest/issues/131",
"repository_url": "https://api.github.com/repos/nikku/testtest",
"html_url": "https://github.com/nikku/testtest/issues/131",
"id": 4411391092,
"node_id": "I_kwDOCPxJEM8AAAABBvB8dA",
"number": 131,
"title": "Sub issue",
"user": {
"login": "nikku",
"id": 58601,
"node_id": "MDQ6VXNlcjU4NjAx",
"avatar_url": "https://avatars.githubusercontent.com/u/58601?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/nikku",
"html_url": "https://github.com/nikku",
"type": "User",
"site_admin": false
},
"labels": [],
"state": "open",
"locked": false,
"assignees": [],
"milestone": null,
"comments": 0,
"created_at": "2026-05-09T06:00:00Z",
"updated_at": "2026-05-09T06:00:01Z",
"closed_at": null,
"assignee": null,
"author_association": "OWNER",
"active_lock_reason": null,
"body": null,
"state_reason": null,
"parent_issue_url": null
},
"repository": {
"id": 150751504,
"node_id": "MDEwOlJlcG9zaXRvcnkxNTA3NTE1MDQ=",
"name": "testtest",
"full_name": "nikku/testtest",
"private": false,
"owner": {
"login": "nikku",
"id": 58601,
"node_id": "MDQ6VXNlcjU4NjAx",
"avatar_url": "https://avatars.githubusercontent.com/u/58601?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/nikku",
"html_url": "https://github.com/nikku",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/nikku/testtest"
},
"installation": {
"id": 48472471,
"node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNDg0NzI0NzE="
}
}
}
Loading