From 5a9f50b9b5f84967c3f14d28aa5d99d7a50fa2f4 Mon Sep 17 00:00:00 2001 From: Atle Lillehovde Date: Tue, 25 Nov 2025 13:33:43 +0100 Subject: [PATCH 1/2] Create deisgnsystem-project-sync.yml Dette er en fil som leter etter issues med prefixet "nve-" og kopierer og oppdaterer de over til et Project som da er i sync med issues. --- .../workflows/deisgnsystem-project-sync.yml | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 .github/workflows/deisgnsystem-project-sync.yml diff --git a/.github/workflows/deisgnsystem-project-sync.yml b/.github/workflows/deisgnsystem-project-sync.yml new file mode 100644 index 00000000..23fe0e7d --- /dev/null +++ b/.github/workflows/deisgnsystem-project-sync.yml @@ -0,0 +1,216 @@ + +name: Designsystem Project Sync + +on: + issues: + types: [opened, edited, labeled, unlabeled, reopened] + workflow_dispatch: + +permissions: + contents: read + issues: write + +env: + # 🔗 Sett til riktig Organization Project (Project v2) URL + PROJECT_URL: https://github.com/orgs/NVE/projects/6 + + # Feltnavn slik de står i Project (kan endres til deres navn) + PROJECT_STATUS_FIELD_NAME: Status + FIGMA_FIELD_NAME: Figma + # Støtt både "VitePress" og "Dokumentasjon" – scriptet finner første som finnes + DOC_FIELD_CANDIDATES: VitePress,Dokumentasjon + + # Mapping fra issue-labels -> Status-feltets verdier (single-select) + MAP_STATUS_SYNCH: | + status: ✅ I synk=>I synk + status: ✏️ Design-endring=>Design-endring + status: 🎨 Kun i design=>Kun i design + status: ⏳ I utvikling=>I utvikling + status: 🚫 Deprecated=>Deprecated + + # Hvilke labels identifiserer komponenter + COMPONENT_LABEL_PREFIX: nve- + +jobs: + add-to-project: + name: Add to Project if labeled with component + runs-on: ubuntu-latest + steps: + - name: Check if any component label is present + id: comp + uses: actions/github-script@v7 + with: + script: | + const labels = (context.payload.issue?.labels || []).map(l => typeof l === 'string' ? l : l.name); + const prefix = process.env.COMPONENT_LABEL_PREFIX; + const match = labels.some(l => l?.startsWith(prefix)); + core.setOutput('matches', match ? 'true' : 'false'); + + - name: Add current issue to project + if: steps.comp.outputs.matches == 'true' + uses: actions/add-to-project@v0.5.0 + with: + project-url: ${{ env.PROJECT_URL }} + github-token: ${{ secrets.PAT_TOKEN }} + + sync-fields: + name: Sync Status + Figma + VitePress/Dokumentasjon + runs-on: ubuntu-latest + steps: + - name: Update Project fields from issue labels & body + uses: actions/github-script@v7 + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + with: + script: | + const { graphql } = require('@octokit/graphql'); + const gql = graphql.defaults({ headers: { authorization: `token ${process.env.GH_TOKEN}` }}); + + const PROJECT_URL = process.env.PROJECT_URL; + const STATUS_FIELD_NAME = process.env.PROJECT_STATUS_FIELD_NAME; + const FIGMA_FIELD_NAME = process.env.FIGMA_FIELD_NAME; + const DOC_FIELD_CANDIDATES = process.env.DOC_FIELD_CANDIDATES.split(',').map(s=>s.trim()); + const map = Object.fromEntries( + process.env.MAP_STATUS_SYNCH.split('\n') + .filter(Boolean) + .map(line => line.split('=>').map(s => s.trim())) + ); + + const issue = context.payload.issue; + if (!issue) { core.info('No issue payload'); return; } + + // 1) Finn ønsket Status-verdi ut fra labels + const labels = issue.labels.map(l => typeof l === 'string' ? l : l.name); + const statusLabel = Object.keys(map).find(k => labels.includes(k)); + const desiredStatus = statusLabel ? map[statusLabel] : null; + + // 2) Parse Figma- og VitePress-lenker fra issue-body + const body = issue.body || ''; + const urlRe = /(https?:\/\/[^\s)]+)/g; + + // Primært: prøv linjer som starter med "Figma:" / "VitePress:" / "Dokumentasjon:" + function extractLabeledUrl(prefix) { + const re = new RegExp(`^\\s*${prefix}\\s*:\\s*(https?://[^\\s]+)`, 'im'); + const m = body.match(re); + return m ? m[1].trim() : null; + } + let figmaUrl = extractLabeledUrl('Figma'); + let docUrl = extractLabeledUrl('VitePress') || extractLabeledUrl('Dokumentasjon'); + + // Sekundært: fallback via domenesøk + if (!figmaUrl) { + const m = body.match(/https?:\/\/(?:www\\.)?figma\\.com\\/[^\s)]+/i); + figmaUrl = m ? m[0] : null; + } + if (!docUrl) { + const m = body.match(/https?:\/\/[\\w.-]*designsystem\\.[\\w.-]+\\/[^\s)]+/i); + docUrl = m ? m[0] : null; + } + + // 3) Parse org og project number fra URL + const url = new URL(PROJECT_URL); + // /orgs//projects/ + const org = url.pathname.split('/')[2]; + const projectNumber = parseInt(url.pathname.split('/')[4], 10); + + // 4) Hent Project, felt og options + const projRes = await gql(` + query($org: String!, $number: Int!) { + organization(login: $org) { + projectV2(number: $number) { + id + fields(first: 100) { + nodes { + ... on ProjectV2FieldCommon { id name } + ... on ProjectV2SingleSelectField { id name options { id name } } + ... on ProjectV2Field { id name dataType } + } + } + } + } + } + `, { org, number: projectNumber }); + + const project = projRes.organization?.projectV2; + if (!project) { core.setFailed(`Project not found at ${PROJECT_URL}`); return; } + + // Finn felter + const fields = project.fields.nodes; + const statusField = fields.find(f => f.name === STATUS_FIELD_NAME && f.options); + const figmaField = fields.find(f => f.name === FIGMA_FIELD_NAME); + const docField = DOC_FIELD_CANDIDATES.map(n => fields.find(f => f.name === n)).find(Boolean); + + // Valider felter + if (desiredStatus && !statusField) core.setFailed(`Status field '${STATUS_FIELD_NAME}' not found or not single-select`); + if (desiredStatus && statusField) { + const opt = statusField.options.find(o => o.name === desiredStatus); + if (!opt) core.setFailed(`Status option '${desiredStatus}' not found`); + } + // 5) Finn issue-id + const issueNode = await gql(` + query($owner:String!, $repo:String!, $number:Int!) { + repository(owner:$owner, name:$repo) { + issue(number:$number) { id } + } + } + `, { owner: context.repo.owner, repo: context.repo.repo, number: issue.number }); + const issueId = issueNode.repository.issue.id; + + // 6) Sørg for at issue er i prosjektet + const itemsRes = await gql(` + query($projectId:ID!) { + node(id:$projectId) { + ... on ProjectV2 { + items(first: 200) { + nodes { id content { ... on Issue { id number } } } + } + } + } + } + `, { projectId: project.id }); + + let itemId = itemsRes.node.items.nodes.find(n => n.content?.id === issueId)?.id; + if (!itemId) { + const addRes = await gql(` + mutation($projectId:ID!, $contentId:ID!) { + addProjectV2ItemById(input:{projectId:$projectId, contentId:$contentId}) { item { id } } + } + `, { projectId: project.id, contentId: issueId }); + itemId = addRes.addProjectV2ItemById.item.id; + } + + // 7) Bygg oppdateringer + const mutations = []; + if (desiredStatus && statusField) { + const optId = statusField.options.find(o => o.name === desiredStatus).id; + mutations.push({ + fieldId: statusField.id, + value: { singleSelectOptionId: optId } + }); + } + if (figmaField && figmaUrl) { + mutations.push({ fieldId: figmaField.id, value: { text: figmaUrl } }); + } + if (docField && docUrl) { + mutations.push({ fieldId: docField.id, value: { text: docUrl } }); + } + + // 8) Kjør oppdateringer + for (const m of mutations) { + await gql(` + mutation($projectId:ID!, $itemId:ID!, $fieldId:ID!, $value:ProjectV2FieldValue!) { + updateProjectV2ItemFieldValue(input:{ + projectId:$projectId, + itemId:$itemId, + fieldId:$fieldId, + value:$value + }) { projectV2Item { id } } + } + `, { projectId: project.id, itemId, fieldId: m.fieldId, value: m.value }); + } + + core.info(`Synced fields for #${issue.number}: ` + + [desiredStatus ? `Status='${desiredStatus}'` : null, + figmaUrl ? `Figma='${figmaUrl}'` : null, + docUrl ? `Doc='${docUrl}'` : null].filter(Boolean).join(', ') + ); From 8b84a7e01ace56dec3447db2d81ac2a341359305 Mon Sep 17 00:00:00 2001 From: Atle Lillehovde Date: Wed, 3 Dec 2025 12:41:38 +0100 Subject: [PATCH 2/2] Update deisgnsystem-project-sync.yml Changed variablename m to externalUrl --- .github/workflows/deisgnsystem-project-sync.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deisgnsystem-project-sync.yml b/.github/workflows/deisgnsystem-project-sync.yml index 23fe0e7d..c6b6acfb 100644 --- a/.github/workflows/deisgnsystem-project-sync.yml +++ b/.github/workflows/deisgnsystem-project-sync.yml @@ -91,20 +91,20 @@ jobs: // Primært: prøv linjer som starter med "Figma:" / "VitePress:" / "Dokumentasjon:" function extractLabeledUrl(prefix) { const re = new RegExp(`^\\s*${prefix}\\s*:\\s*(https?://[^\\s]+)`, 'im'); - const m = body.match(re); - return m ? m[1].trim() : null; + const externalUrl = body.match(re); + return externalUrl ? externalUrl[1].trim() : null; } let figmaUrl = extractLabeledUrl('Figma'); let docUrl = extractLabeledUrl('VitePress') || extractLabeledUrl('Dokumentasjon'); // Sekundært: fallback via domenesøk if (!figmaUrl) { - const m = body.match(/https?:\/\/(?:www\\.)?figma\\.com\\/[^\s)]+/i); - figmaUrl = m ? m[0] : null; + const externalUrl = body.match(/https?:\/\/(?:www\\.)?figma\\.com\\/[^\s)]+/i); + figmaUrl = externalUrl ? externalUrl[0] : null; } if (!docUrl) { - const m = body.match(/https?:\/\/[\\w.-]*designsystem\\.[\\w.-]+\\/[^\s)]+/i); - docUrl = m ? m[0] : null; + const externalUrl = body.match(/https?:\/\/[\\w.-]*designsystem\\.[\\w.-]+\\/[^\s)]+/i); + docUrl = externalUrl ? externalUrl[0] : null; } // 3) Parse org og project number fra URL