diff --git a/.github/scripts/sync-docs-project.js b/.github/scripts/sync-docs-project.js index 7d7209c..708a623 100644 --- a/.github/scripts/sync-docs-project.js +++ b/.github/scripts/sync-docs-project.js @@ -19,12 +19,24 @@ function stripMarkdown(value) { return trimmed.replace(/^`|`$/g, "").replace(/^<|>$/g, "").trim(); } -function readBodyValue(body, label) { +function readBodyField(body, label) { const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const match = String(body || "").match( new RegExp(`^[ \\t]*(?:-[ \\t]*)?${escaped}:[ \\t]*(.*)$`, "im"), ); - return match ? stripMarkdown(match[1]) : ""; + return match ? stripMarkdown(match[1]) : undefined; +} + +function readBodyValue(body, label) { + return readBodyField(body, label) ?? ""; +} + +function readFirstBodyValue(body, labels) { + for (const label of labels) { + const value = readBodyField(body, label); + if (value !== undefined) return value; + } + return undefined; } function labelSuffix(labels, prefix) { @@ -45,10 +57,10 @@ function parseIssueMetadata(issue) { const labels = issue.labels || []; const pagePath = readBodyValue(issue.body, "Pfad"); const gitBookUrl = readBodyValue(issue.body, "GitBook URL"); - const sprachreviewCr = readBodyValue(issue.body, "DE Sprache"); - const optimierungsCr = readBodyValue(issue.body, "Optimierung"); - const fachcheckCr = readBodyValue(issue.body, "Fachcheck"); - const enSyncCr = readBodyValue(issue.body, "EN Sync"); + const sprachreviewCr = readFirstBodyValue(issue.body, ["DE Sprache", "DE Sprachreview"]); + const optimierungsCr = readFirstBodyValue(issue.body, ["Optimierung", "DE Optimierung"]); + const fachcheckCr = readFirstBodyValue(issue.body, ["Fachcheck", "DE Fachcheck"]); + const enSyncCr = readFirstBodyValue(issue.body, ["EN Sync"]); if (!pagePath) throw new Error("Issue body is missing 'Pfad'."); if (!gitBookUrl) throw new Error("Issue body is missing 'GitBook URL'."); @@ -201,6 +213,11 @@ function buildFieldUpdates(project, metadata) { return { singleSelect, text }; } +function getTextFieldAction(value, required) { + if (value === undefined || (required && !value)) return "skip"; + return value ? "set" : "clear"; +} + async function run({ github, context, core, issueNumber, organization, projectNumber }) { if (!Number.isInteger(issueNumber) || issueNumber < 1) { throw new Error("issue_number must be a positive integer."); @@ -232,9 +249,10 @@ async function run({ github, context, core, issueNumber, organization, projectNu } for (const { field, value, required } of updates.text) { - if (value) { + const action = getTextFieldAction(value, required); + if (action === "set") { await setFieldValue(github, project.id, itemId, field.id, { text: value }); - } else if (!required) { + } else if (action === "clear") { await clearFieldValue(github, project.id, itemId, field.id); } } @@ -264,6 +282,7 @@ module.exports = { buildFieldUpdates, findField, findOption, + getTextFieldAction, normalize, parseIssueMetadata, readBodyValue, diff --git a/.github/scripts/sync-docs-project.test.js b/.github/scripts/sync-docs-project.test.js index 1fd5639..0c51148 100644 --- a/.github/scripts/sync-docs-project.test.js +++ b/.github/scripts/sync-docs-project.test.js @@ -4,6 +4,7 @@ const test = require("node:test"); const { buildFieldUpdates, findOption, + getTextFieldAction, normalize, parseIssueMetadata, readBodyValue, @@ -70,6 +71,35 @@ test("reads markdown links and backtick values", () => { ); }); +test("accepts existing issue CR headings used by editors", () => { + const result = parseIssueMetadata({ + body: `Pfad: example +GitBook URL: https://example.com +- DE Sprachreview: https://example.com/sprache +- DE Optimierung: https://example.com/optimierung +- DE Fachcheck: Nicht erforderlich – Fachcheck ohne Änderungsbedarf abgeschlossen.`, + labels: ["stage:en-sync", "lang:de", "area:loslegen"], + }); + + assert.equal(result.sprachreviewCr, "https://example.com/sprache"); + assert.equal(result.optimierungsCr, "https://example.com/optimierung"); + assert.equal(result.fachcheckCr, "Nicht erforderlich – Fachcheck ohne Änderungsbedarf abgeschlossen."); +}); + +test("omitted CR headings preserve project values while blank headings clear them", () => { + const labels = ["stage:en-sync", "lang:de", "area:loslegen"]; + const base = "Pfad: example\nGitBook URL: https://example.com"; + + const omitted = parseIssueMetadata({ body: base, labels }); + const blank = parseIssueMetadata({ body: base + "\n- Fachcheck:", labels }); + + assert.equal(omitted.fachcheckCr, undefined); + assert.equal(blank.fachcheckCr, ""); + assert.equal(getTextFieldAction(omitted.fachcheckCr, false), "skip"); + assert.equal(getTextFieldAction(blank.fachcheckCr, false), "clear"); + assert.equal(getTextFieldAction("https://example.com/cr", false), "set"); +}); + test("matches labels to project options independent of punctuation and case", () => { assert.equal(normalize("DE Sprachreview"), normalize("de-sprachreview")); assert.equal( diff --git a/.github/workflows/sync-docs-project.yml b/.github/workflows/sync-docs-project.yml index a55759b..40ff9f3 100644 --- a/.github/workflows/sync-docs-project.yml +++ b/.github/workflows/sync-docs-project.yml @@ -1,6 +1,11 @@ name: Sync docs issue to project on: + pull_request: + paths: + - ".github/scripts/sync-docs-project.js" + - ".github/scripts/sync-docs-project.test.js" + - ".github/workflows/sync-docs-project.yml" issues: types: [opened, edited, labeled, unlabeled, reopened] workflow_dispatch: @@ -15,12 +20,21 @@ permissions: issues: read concurrency: - group: docs-project-sync-${{ github.event.issue.number || inputs.issue_number }} + group: docs-project-sync-${{ github.event.issue.number || inputs.issue_number || github.event.pull_request.number || github.run_id }} cancel-in-progress: true jobs: + test: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Test metadata parser + run: node --test .github/scripts/sync-docs-project.test.js + sync: - if: github.event_name == 'workflow_dispatch' || contains(github.event.issue.labels.*.name, 'docs-overhaul') + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'docs-overhaul')) runs-on: ubuntu-latest env: ISSUE_NUMBER: ${{ inputs.issue_number || github.event.issue.number }}