Skip to content

Commit c5863e2

Browse files
authored
Merge branch 'main' into chart-colors
2 parents dc022a2 + 5c0515d commit c5863e2

88 files changed

Lines changed: 10166 additions & 8913 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-and-deploy-doc-site.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ on:
1616

1717
jobs:
1818
build_and_deploy_job:
19+
permissions:
20+
contents: read
21+
pull-requests: write
1922
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
2023
runs-on: ubuntu-latest
2124
name: Bygg og installer
@@ -54,6 +57,8 @@ jobs:
5457
skip_api_build: true # Dette nettstedet trenger ikke noe API
5558

5659
close_pull_request_job:
60+
permissions:
61+
contents: read
5762
if: github.event_name == 'pull_request' && github.event.action == 'closed'
5863
runs-on: ubuntu-latest
5964
name: Lukker pull request og fjerner testmiljø for denne PR
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
name: Designsystem Project Sync
2+
3+
on:
4+
issues:
5+
types: [opened, edited, labeled, unlabeled, reopened]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
issues: write
11+
12+
env:
13+
# 🔗 Sett til riktig Organization Project (Project v2) URL
14+
PROJECT_URL: "https://github.com/orgs/NVE/projects/6"
15+
16+
# 📌 Feltnavn slik de står i Project (case-sensitive)
17+
PROJECT_STATUS_FIELD_NAME: "Status"
18+
FIGMA_FIELD_NAME: "Figma"
19+
# Scriptet leter etter første felt som matcher en av disse navnene
20+
DOC_FIELD_CANDIDATES: "VitePress,Dokumentasjon"
21+
22+
# 🔁 Mapping fra issue-labels -> Status-feltets verdier (single-select)
23+
# Venstre side er nøyaktig labelnavn; høyre side er nøyaktig option-verdi i Status-feltet.
24+
MAP_STATUS_SYNCH: |
25+
status: ✅ I synk=>I synk
26+
status: ✏️ Design-endring=>Design-endring
27+
status: 🎨 Kun i design=>Kun i design
28+
status: ⏳ I utvikling=>I utvikling
29+
status: 🚫 Deprecated=>Deprecated
30+
31+
# 🏷️ Hvilke labels identifiserer komponenter (prefiks)
32+
COMPONENT_LABEL_PREFIX: "nve-"
33+
34+
jobs:
35+
add-to-project:
36+
name: Add to Project if labeled with component
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Check if any component label is present
40+
id: comp
41+
uses: actions/github-script@v7
42+
with:
43+
script: |
44+
const labels = (context.payload.issue?.labels || [])
45+
.map(l => typeof l === 'string' ? l : l.name);
46+
const prefix = process.env.COMPONENT_LABEL_PREFIX;
47+
const match = labels.some(l => l?.startsWith(prefix));
48+
core.setOutput('matches', match ? 'true' : 'false');
49+
50+
- name: Add current issue to project
51+
if: steps.comp.outputs.matches == 'true'
52+
uses: actions/add-to-project@v0.5.0
53+
with:
54+
project-url: ${{ env.PROJECT_URL }}
55+
github-token: ${{ secrets.PAT_TOKEN }}
56+
57+
- name: Update Project fields from issue labels & body
58+
if: steps.comp.outputs.matches == 'true'
59+
uses: actions/github-script@v7
60+
with:
61+
github-token: ${{ secrets.PAT_TOKEN }}
62+
script: |
63+
// Ferdig autentisert GraphQL-klient er tilgjengelig via github.graphql
64+
const gql = github.graphql;
65+
try {
66+
// --- ENV / konfig ---
67+
const PROJECT_URL = process.env.PROJECT_URL;
68+
const STATUS_FIELD_NAME = process.env.PROJECT_STATUS_FIELD_NAME;
69+
const FIGMA_FIELD_NAME = process.env.FIGMA_FIELD_NAME;
70+
const DOC_FIELD_CANDIDATES = (process.env.DOC_FIELD_CANDIDATES || '')
71+
.split(',')
72+
.map(s => s.trim())
73+
.filter(Boolean);
74+
75+
const mapLines = (process.env.MAP_STATUS_SYNCH || '')
76+
.split('\n')
77+
.map(l => l.trim())
78+
.filter(Boolean);
79+
80+
const statusMap = Object.fromEntries(
81+
mapLines.map(line => {
82+
const parts = line.split('=>');
83+
if (parts.length !== 2) return ['__INVALID__', ''];
84+
return [parts[0].trim(), parts[1].trim()];
85+
}).filter(([k]) => k !== '__INVALID__')
86+
);
87+
88+
const issue = context.payload.issue;
89+
if (!issue) {
90+
core.info('No issue in payload; skip');
91+
return;
92+
}
93+
94+
// --- Status fra labels ---
95+
const labels = (issue.labels || []).map(l => typeof l === 'string' ? l : l.name);
96+
const statusLabel = Object.keys(statusMap).find(k => labels.includes(k)) || null;
97+
const desiredStatus = statusLabel ? statusMap[statusLabel] : null;
98+
99+
// --- Ekstraher Figma & Doc fra body ---
100+
const body = issue.body || '';
101+
102+
const rFigma = /^\s*Figma\s*:\s*(https?:\/\/\S+)/im;
103+
let figmaUrl = null;
104+
let docUrl = null;
105+
106+
const mF = body.match(rFigma);
107+
if (mF) figmaUrl = (mF[1] || '').trim();
108+
109+
if (DOC_FIELD_CANDIDATES.length > 0) {
110+
// Bygg regex trygt (escape kandidatnavn)
111+
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
112+
const pattern = new RegExp(
113+
`^\\s*(${DOC_FIELD_CANDIDATES.map(esc).join('|')})\\s*:\\s*(https?:\\/\\/\\S+)`,
114+
'im'
115+
);
116+
const mD = body.match(pattern);
117+
if (mD) docUrl = (mD[2] || '').trim();
118+
}
119+
120+
// Fallback: finn første figma/dokumentasjons-URL i body
121+
if (!figmaUrl) {
122+
const dm = body.match(/https?:\/\/(?:www\.)?figma\.com\/\S+/i);
123+
if (dm) figmaUrl = dm[0];
124+
}
125+
if (!docUrl) {
126+
const dm = body.match(/https?:\/\/[A-Za-z0-9._-]*designsystem\.[A-Za-z0-9._-]+\/\S+/i);
127+
if (dm) docUrl = dm[0];
128+
}
129+
130+
// --- Parse org/prosjektnummer ---
131+
const u = new URL(PROJECT_URL);
132+
// Forventet: /orgs/<org>/projects/<number>
133+
const parts = u.pathname.split('/').filter(Boolean);
134+
const org = parts[1];
135+
const projectNumber = parseInt(parts[3], 10);
136+
if (!org || Number.isNaN(projectNumber)) {
137+
core.setFailed(`Could not parse org/project from PROJECT_URL='${PROJECT_URL}'`);
138+
return;
139+
}
140+
141+
// --- Hent Project & felter ---
142+
const projRes = await gql(
143+
`
144+
query($org: String!, $number: Int!) {
145+
organization(login: $org) {
146+
projectV2(number: $number) {
147+
id
148+
fields(first: 100) {
149+
nodes {
150+
... on ProjectV2FieldCommon { id name }
151+
... on ProjectV2SingleSelectField { id name options { id name } }
152+
... on ProjectV2Field { id name dataType }
153+
}
154+
}
155+
}
156+
}
157+
}`,
158+
{ org, number: projectNumber }
159+
);
160+
161+
const project = projRes.organization?.projectV2;
162+
if (!project) {
163+
core.setFailed(`Project not found at ${PROJECT_URL}`);
164+
return;
165+
}
166+
167+
const fields = project.fields.nodes || [];
168+
const statusField = fields.find(f => f.name === STATUS_FIELD_NAME && Array.isArray(f.options));
169+
const figmaField = fields.find(f => f.name === FIGMA_FIELD_NAME);
170+
const docField = DOC_FIELD_CANDIDATES.map(n => fields.find(f => f.name === n)).find(Boolean);
171+
172+
// --- Valider status-option ---
173+
let statusOptionId = null;
174+
if (desiredStatus) {
175+
if (!statusField) {
176+
core.setFailed(`Status field '${STATUS_FIELD_NAME}' not found or not single-select`);
177+
return;
178+
}
179+
const opt = statusField.options.find(o => o.name === desiredStatus);
180+
if (!opt) {
181+
core.setFailed(`Status option '${desiredStatus}' not found in project`);
182+
return;
183+
}
184+
statusOptionId = opt.id;
185+
}
186+
187+
// --- Finn Issue-ID ---
188+
const issueNode = await gql(
189+
`
190+
query($owner:String!, $repo:String!, $number:Int!) {
191+
repository(owner:$owner, name:$repo) {
192+
issue(number:$number) { id }
193+
}
194+
}`,
195+
{ owner: context.repo.owner, repo: context.repo.repo, number: issue.number }
196+
);
197+
198+
const issueId = issueNode.repository?.issue?.id;
199+
if (!issueId) {
200+
core.setFailed(`Could not resolve issue id for #${issue.number}`);
201+
return;
202+
}
203+
204+
// --- Finn/legg til Project Item ---
205+
const itemsRes = await gql(
206+
`
207+
query($projectId:ID!) {
208+
node(id:$projectId) {
209+
... on ProjectV2 {
210+
items(first: 200) {
211+
nodes {
212+
id
213+
content { ... on Issue { id number } }
214+
}
215+
}
216+
}
217+
}
218+
}`,
219+
{ projectId: project.id }
220+
);
221+
222+
let itemId = itemsRes.node?.items?.nodes?.find(n => n.content?.id === issueId)?.id || null;
223+
224+
if (!itemId) {
225+
const addRes = await gql(
226+
`
227+
mutation($projectId:ID!, $contentId:ID!) {
228+
addProjectV2ItemById(input:{projectId:$projectId, contentId:$contentId}) {
229+
item { id }
230+
}
231+
}`,
232+
{ projectId: project.id, contentId: issueId }
233+
);
234+
itemId = addRes.addProjectV2ItemById?.item?.id || null;
235+
if (!itemId) {
236+
core.setFailed('Failed to add item to project');
237+
return;
238+
}
239+
}
240+
241+
// --- Samle oppdateringer ---
242+
const updates = [];
243+
if (statusOptionId) updates.push({ fieldId: statusField.id, value: { singleSelectOptionId: statusOptionId } });
244+
if (figmaField && figmaUrl) updates.push({ fieldId: figmaField.id, value: { text: figmaUrl } });
245+
if (docField && docUrl) updates.push({ fieldId: docField.id, value: { text: docUrl } });
246+
247+
// --- Utfør oppdateringer ---
248+
for (const upd of updates) {
249+
await gql(
250+
`
251+
mutation($projectId:ID!, $itemId:ID!, $fieldId:ID!, $value:ProjectV2FieldValue!) {
252+
updateProjectV2ItemFieldValue(input:{
253+
projectId:$projectId,
254+
itemId:$itemId,
255+
fieldId:$fieldId,
256+
value:$value
257+
}) {
258+
projectV2Item { id }
259+
}
260+
}`,
261+
{ projectId: project.id, itemId, fieldId: upd.fieldId, value: upd.value }
262+
);
263+
}
264+
265+
core.info(
266+
`Synced #${issue.number}: ` +
267+
[
268+
statusOptionId ? `Status='${desiredStatus}'` : null,
269+
figmaUrl ? `Figma='${figmaUrl}'` : null,
270+
docUrl ? `Doc='${docUrl}'` : null
271+
].filter(Boolean).join(', ')
272+
);
273+
} catch (e) {
274+
core.setFailed(`Script failed: ${e.stack || e.message}`);
275+
}

0 commit comments

Comments
 (0)