diff --git a/components/grant-application/steps/ProjectDetails.tsx b/components/grant-application/steps/ProjectDetails.tsx
index 96690696a..1595d6d6a 100644
--- a/components/grant-application/steps/ProjectDetails.tsx
+++ b/components/grant-application/steps/ProjectDetails.tsx
@@ -19,6 +19,7 @@ export default function ProjectDetails({ register, watch, errors }: StepProps) {
+
diff --git a/pages/api/github.ts b/pages/api/github.ts
index 481124b16..f29b819a2 100644
--- a/pages/api/github.ts
+++ b/pages/api/github.ts
@@ -1,4 +1,9 @@
import { NextApiRequest, NextApiResponse } from 'next/types'
+import {
+ getApplicationIssueLabels,
+ getApplicationRepo,
+ normalizeMainFocus,
+} from '../../utils/application-routing'
const GH_ACCESS_TOKEN = process.env.GH_ACCESS_TOKEN
const GH_ORG = process.env.GH_ORG
@@ -134,48 +139,10 @@ ${req.body.video_application ? req.body.video_application : 'None provided.'}
${req.body.anything_else ? req.body.anything_else : 'No.'}
${contactFooter}`
- // Label set according to "main focus" (absent for RED applications)
- const mainFocus = req.body.main_focus
- ? `${req.body.main_focus}`.toLowerCase()
- : ''
- const issueLabels = mainFocus ? [mainFocus] : []
- if (mainFocus === 'layer1' || mainFocus === 'layer2') {
- issueLabels.push('bitcoin') // L1 & L2 = subset of Bitcoin
- }
-
- // Add label for applications from common grant app
- if (req.body.source === 'common-grant-app') {
- issueLabels.push('common-grant-app')
- }
-
- // Repo set according to "main focus"
- let appRepo = GH_APP_REPO
- if (mainFocus === 'nostr') {
- appRepo = `${GH_APP_REPO}-nostr`
- }
- if (mainFocus === 'layer1') {
- appRepo = `${GH_APP_REPO}-layer1`
- }
- if (mainFocus === 'layer2') {
- appRepo = `${GH_APP_REPO}-layer2`
- }
- if (mainFocus === 'core') {
- appRepo = `${GH_APP_REPO}-core`
- }
- if (mainFocus === 'ecash') {
- appRepo = `${GH_APP_REPO}-ecash`
- }
-
- // Tag depending on request for grant and/or request for listing
- req.body.LTS && issueLabels.push('LTS')
- req.body.RED && issueLabels.push('RED')
-
- // Additional tags based on yes/no answers
- req.body.has_received_funding === 'yes' && issueLabels.push('prior funding')
- if (!req.body.RED) {
- !req.body.free_open_source && issueLabels.push('not FLOSS')
- !req.body.are_you_lead && issueLabels.push('surrogate')
- }
+ // Labels and repo are set according to main focus.
+ const mainFocus = normalizeMainFocus(req.body.main_focus)
+ const issueLabels = getApplicationIssueLabels(req.body)
+ const appRepo = getApplicationRepo(GH_APP_REPO, mainFocus)
try {
await octokit.rest.issues.create({
diff --git a/public/static/opensats-grant-application-template.md b/public/static/opensats-grant-application-template.md
index 74440bfc8..a8b180e49 100644
--- a/public/static/opensats-grant-application-template.md
+++ b/public/static/opensats-grant-application-template.md
@@ -9,7 +9,7 @@ application at https://opensats.org/apply/grant
**Project Name:**
-**Main Focus:** (Bitcoin, Nostr, Layer 1, Layer 2, Core, Ecash, Education, Other)
+**Main Focus:** (Bitcoin, Nostr, Layer 1, Layer 2, Core, Ecash, Education, Security, Other)
### Project Description
diff --git a/utils/application-routing.test.js b/utils/application-routing.test.js
new file mode 100644
index 000000000..b86ee4543
--- /dev/null
+++ b/utils/application-routing.test.js
@@ -0,0 +1,38 @@
+/** @jest-environment node */
+/* eslint-env jest, node */
+
+const {
+ getApplicationIssueLabels,
+ getApplicationRepo,
+ normalizeMainFocus,
+} = require('./application-routing.ts')
+
+describe('application routing', () => {
+ it('normalizes main focus values', () => {
+ expect(normalizeMainFocus('Security')).toBe('security')
+ expect(normalizeMainFocus(undefined)).toBe('')
+ })
+
+ it('labels regular security grant applications', () => {
+ expect(
+ getApplicationIssueLabels({
+ main_focus: 'security',
+ free_open_source: true,
+ are_you_lead: true,
+ })
+ ).toEqual(['security'])
+ })
+
+ it('keeps security applications in the default applications repo', () => {
+ expect(getApplicationRepo('applications', 'security')).toBe('applications')
+ })
+
+ it('routes known focus-specific applications to their repos', () => {
+ expect(getApplicationRepo('applications', 'layer1')).toBe(
+ 'applications-layer1'
+ )
+ expect(getApplicationIssueLabels({ main_focus: 'layer2' })).toContain(
+ 'bitcoin'
+ )
+ })
+})
diff --git a/utils/application-routing.ts b/utils/application-routing.ts
new file mode 100644
index 000000000..0e845d01d
--- /dev/null
+++ b/utils/application-routing.ts
@@ -0,0 +1,41 @@
+const REPO_SUFFIX_BY_MAIN_FOCUS: Record = {
+ core: '-core',
+ ecash: '-ecash',
+ layer1: '-layer1',
+ layer2: '-layer2',
+ nostr: '-nostr',
+}
+
+export function normalizeMainFocus(mainFocus: unknown) {
+ return mainFocus ? `${mainFocus}`.toLowerCase() : ''
+}
+
+export function getApplicationRepo(baseRepo: string, mainFocus: unknown) {
+ return `${baseRepo}${
+ REPO_SUFFIX_BY_MAIN_FOCUS[normalizeMainFocus(mainFocus)] || ''
+ }`
+}
+
+export function getApplicationIssueLabels(body: Record) {
+ const mainFocus = normalizeMainFocus(body.main_focus)
+ const issueLabels = mainFocus ? [mainFocus] : []
+
+ if (mainFocus === 'layer1' || mainFocus === 'layer2') {
+ issueLabels.push('bitcoin')
+ }
+
+ if (body.source === 'common-grant-app') {
+ issueLabels.push('common-grant-app')
+ }
+
+ if (body.LTS) issueLabels.push('LTS')
+ if (body.RED) issueLabels.push('RED')
+ if (body.has_received_funding === 'yes') issueLabels.push('prior funding')
+
+ if (!body.RED) {
+ if (!body.free_open_source) issueLabels.push('not FLOSS')
+ if (!body.are_you_lead) issueLabels.push('surrogate')
+ }
+
+ return issueLabels
+}