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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Konfiguracja:

- Lista obserwowanych workflow jest w `discord-notify.yml` każdego repozytorium (`workflows:`). Nowy workflow trzeba tam dopisać po nazwie (pole `name:`), na `main` i `develop`. `workflow_run` działa tylko z kopii na `main`.
- Webhook Discorda to sekret repozytorium `DISCORD_WEBHOOK_URL` w każdym z czterech repozytoriów. Sekret organizacji nie wystarczy: na darmowym planie nie widzą go repozytoria prywatne.
- Worker: `discord-relay/wrangler.jsonc` (projekt, pola, przekazywanie issue i PR), sekrety `DISCORD_WEBHOOK_URL`, `GITHUB_WEBHOOK_SECRET` i opcjonalny `GITHUB_TOKEN`. Webhook organizacji wskazuje na `https://tuttitrip-hooks.gburek.app/github`.
- Worker: `discord-relay/wrangler.jsonc` (projekt, pola, przekazywanie issue i PR), sekrety `DISCORD_WEBHOOK_URL` i `GITHUB_WEBHOOK_SECRET` (bez tokenu GitHub, więc wiadomość o elemencie projektu ma link zamiast tytułu). Webhook organizacji wskazuje na `https://tuttitrip-hooks.gburek.app/github`.

Zmiana (rotacja) webhooka Discorda: utwórz nowy webhook w ustawieniach kanału i usuń stary, potem z pliku z nowym URL-em:

Expand Down
18 changes: 5 additions & 13 deletions discord-relay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,12 @@ treści `{"posted": true, "discord": 204}` albo powód pominięcia, więc w
| --- | --- |
| `DISCORD_WEBHOOK_URL` | URL webhooka kanału Discord (ten sam co w sekretach repozytoriów) |
| `GITHUB_WEBHOOK_SECRET` | losowy sekret, ten sam w ustawieniach webhooka organizacji |
| `GITHUB_TOKEN` (opcjonalny) | fine-grained PAT tylko do odczytu, opis niżej |

Bez `GITHUB_TOKEN` wiadomość ma typ elementu i link do niego w projekcie, ale
bez tytułu. Z tokenem Worker pobiera z GraphQL tytuł, numer, repozytorium i
bieżący Status. Token: GitHub, Settings, Developer settings, Fine-grained
tokens, Generate new token. Resource owner `HackYeah-TuttiTripTeam`,
Repository access "All repositories", uprawnienia repozytorium Issues: Read,
Pull requests: Read (Metadata: Read dochodzi samo), uprawnienia organizacji
Projects: Read. Potem:

```bash
cd discord-relay
npx wrangler@4.147.0 secret put GITHUB_TOKEN # wklej token w prompt
```
Worker nie używa tokenu GitHub. Zdarzenie elementu projektu nie zawiera
tytułu, więc wiadomość podaje typ (Issue, Pull request, Szkic), kto zmienił,
starą i nową wartość pola oraz link do elementu w projekcie. Nowe issue trafiają
do projektu przez formularze zgłoszeń (`projects:`) i wbudowany auto-add, nie
przez ten Worker.

## Wdrożenie

Expand Down
18 changes: 6 additions & 12 deletions discord-relay/src/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ function message(embed) {
}

/**
* projects_v2_item -> Discord message, or { skip: reason }.
* `info` (optional, from GraphQL): { title, url, number, repo, state, status }.
* projects_v2_item -> Discord message, or { skip: reason }. The payload has no
* item title (that would need a GitHub token), so the message links to the item.
*/
export function projectItemMessage(payload, env, info = {}) {
export function projectItemMessage(payload, env) {
const item = payload.projects_v2_item
const action = payload.action
const projectUrl = env.PROJECT_URL
Expand Down Expand Up @@ -123,21 +123,15 @@ export function projectItemMessage(payload, env, info = {}) {
return { skip: `action ${action} not posted` } // reordered
}

const title = info.title
? plain(info.number ? `#${info.number} ${info.title}` : info.title)
: `${kind} (element ${item.id})`
const fields = [
{ name: 'Typ', value: kind, inline: true },
info.repo && { name: 'Repozytorium', value: esc(info.repo, 100), inline: true },
{ name: 'Kto', value: userLink(payload.sender), inline: true },
info.status &&
!description.includes('**Status**') && { name: 'Status', value: esc(info.status, 100), inline: true },
].filter(Boolean)
]

return message({
author: { name: 'Projekt TuttiTrip', url: projectUrl },
title,
url: action === 'deleted' ? info.url || projectUrl : info.url || paneUrl,
title: `${kind} w projekcie (element ${item.id})`,
url: action === 'deleted' ? projectUrl : paneUrl,
description: cut(description, 4000),
color,
fields,
Expand Down
60 changes: 4 additions & 56 deletions discord-relay/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
// closed / merged issues and PRs. Workflow runs are not handled here (GitHub
// Actions posts them itself, see .github/workflows/discord-notify.yml).
//
// Secrets: DISCORD_WEBHOOK_URL, GITHUB_WEBHOOK_SECRET, optional GITHUB_TOKEN
// (fine-grained PAT, read-only: titles and current Status of project items).
// Secrets: DISCORD_WEBHOOK_URL, GITHUB_WEBHOOK_SECRET. No GitHub token: messages
// use only what the webhook payload carries (a project item event has no title,
// so the message links to the item in the project).
import {
issueMessage,
projectItemMessage,
Expand Down Expand Up @@ -40,59 +41,6 @@ export async function verifySignature(secret, body, header) {
return crypto.subtle.verify('HMAC', key, signature, body)
}

const ITEM_QUERY = `query($ids: [ID!]!) {
nodes(ids: $ids) {
__typename
... on ProjectV2Item {
fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue { name } }
}
... on DraftIssue { title }
... on Issue { title number url repository { name } }
... on PullRequest { title number url repository { name } }
}
}`

/** Title, link and current Status of a project item. Empty without a token or on any error. */
async function itemInfo(env, item) {
if (!env.GITHUB_TOKEN) return {}
try {
const res = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
authorization: `Bearer ${env.GITHUB_TOKEN}`,
'content-type': 'application/json',
'user-agent': 'tuttitrip-discord-relay',
},
body: JSON.stringify({
query: ITEM_QUERY,
variables: { ids: [item.node_id, item.content_node_id].filter(Boolean) },
}),
signal: AbortSignal.timeout(5000),
})
if (!res.ok) {
console.log(JSON.stringify({ graphql: res.status }))
return {}
}
// Deleted items/drafts come back as null nodes with NOT_FOUND errors.
const nodes = (await res.json()).data?.nodes ?? []
const info = {}
for (const node of nodes) {
if (!node) continue
if (node.__typename === 'ProjectV2Item') info.status = node.fieldValueByName?.name
else {
info.title = node.title
info.number = node.number
info.url = node.url
info.repo = node.repository?.name
}
}
return info
} catch (e) {
console.log(JSON.stringify({ graphql: e.name }))
return {}
}
}

async function postToDiscord(env, message) {
const post = () =>
fetch(env.DISCORD_WEBHOOK_URL, {
Expand All @@ -118,7 +66,7 @@ async function buildMessage(event, payload, env) {
const item = payload.projects_v2_item
if (!anyProject && item.project_node_id !== env.PROJECT_NODE_ID) return { skip: 'other project' }
if (payload.action === 'reordered') return { skip: 'reordered' }
return projectItemMessage(payload, env, await itemInfo(env, item))
return projectItemMessage(payload, env)
}
case 'projects_v2':
if (!anyProject && payload.projects_v2?.node_id !== env.PROJECT_NODE_ID) return { skip: 'other project' }
Expand Down
3 changes: 1 addition & 2 deletions discord-relay/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,5 @@
// Also post opened/closed/merged issues and PRs (needs those events on the org webhook).
"FORWARD_ISSUES_PRS": "true"
}
// Secrets (wrangler secret put): DISCORD_WEBHOOK_URL, GITHUB_WEBHOOK_SECRET,
// optional GITHUB_TOKEN (fine-grained PAT, read-only, for item titles).
// Secrets (wrangler secret put): DISCORD_WEBHOOK_URL, GITHUB_WEBHOOK_SECRET.
}
Loading