From a3c336ff8eb0d57afbbfbcb36b7a28db60f7e874 Mon Sep 17 00:00:00 2001 From: philmillman Date: Fri, 21 Aug 2026 11:56:12 -0400 Subject: [PATCH 1/2] feedback links in footer and card pages --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/ISSUE_TEMPLATE/card_feedback.yml | 34 +++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 1 + .github/ISSUE_TEMPLATE/website_feedback.yml | 33 ++++++ .github/workflows/pages.yml | 12 +++ package.json | 1 + tools/sitegen/package.json | 1 + tools/sitegen/src/curation/cli.js | 2 +- tools/sitegen/src/github/syncCardLabels.js | 87 ++++++++++++++++ tools/sitegen/src/render/cardPage.js | 6 +- tools/sitegen/src/render/githubIssue.js | 105 ++++++++++++++++++++ tools/sitegen/src/render/layout.js | 3 +- tools/sitegen/test/githubIssue.test.js | 98 ++++++++++++++++++ tools/sitegen/test/render.test.js | 43 ++++++++ 14 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/card_feedback.yml create mode 100644 .github/ISSUE_TEMPLATE/website_feedback.yml create mode 100644 tools/sitegen/src/github/syncCardLabels.js create mode 100644 tools/sitegen/src/render/githubIssue.js create mode 100644 tools/sitegen/test/githubIssue.test.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 006abac23..38f2d87d4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,6 +8,7 @@ body: value: | Thanks for taking the time to report a bug. + **A specific program card** should use the Program card template, or Send feedback on that card's page. **New program cards** belong under `releases/` via a pull request — see the [README](https://github.com/TomWhitwell/Workshop_Computer/blob/main/README.md). Do not use this template to propose a new card. - type: input id: summary diff --git a/.github/ISSUE_TEMPLATE/card_feedback.yml b/.github/ISSUE_TEMPLATE/card_feedback.yml new file mode 100644 index 000000000..f7c625a63 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/card_feedback.yml @@ -0,0 +1,34 @@ +name: Program card +description: Report a problem or suggest an improvement for a specific program card +title: "[Card]: " +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report this. + + **Chat and designer questions** belong in [Discord](https://discord.com/channels/1210238368898879569/1484219323039092938). + **New program cards** belong under `releases/` via a pull request — see the [README](https://github.com/TomWhitwell/Workshop_Computer/blob/main/README.md). + **Catalogue / website issues** should use the Website feedback template. + - type: input + id: card + attributes: + label: Card + description: Which program card is this about? (prefilled from the card page) + placeholder: 03 Turing Machine + validations: + required: true + - type: input + id: summary + attributes: + label: Summary + description: A short description of the problem or idea + validations: + required: true + - type: textarea + id: details + attributes: + label: Details + description: What happened, or what should change? Include firmware version, steps, or screenshots if they help. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 243122263..6ce8a6951 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -8,6 +8,7 @@ body: value: | Thanks for suggesting an improvement. + **A specific program card** should use the Program card template, or Send feedback on that card's page. **New program cards** belong under `releases/` via a pull request — see the [README](https://github.com/TomWhitwell/Workshop_Computer/blob/main/README.md). Do not use this template to propose a new card. - type: input id: summary diff --git a/.github/ISSUE_TEMPLATE/website_feedback.yml b/.github/ISSUE_TEMPLATE/website_feedback.yml new file mode 100644 index 000000000..f4165e6a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/website_feedback.yml @@ -0,0 +1,33 @@ +name: Website feedback +description: Report a problem or suggest an improvement for the program-card catalogue site +title: "[Website]: " +labels: ["Website"] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve [computer.musicthing.co.uk](https://computer.musicthing.co.uk/). + + **Chat and designer questions** belong in [Discord](https://discord.com/channels/1210238368898879569/1484219323039092938). + **A specific program card** should use the Program card template, or Send feedback on that card's page. + **New program cards** belong under `releases/` via a pull request — see the [README](https://github.com/TomWhitwell/Workshop_Computer/blob/main/README.md). + - type: input + id: summary + attributes: + label: Summary + description: A short description of the problem or idea + validations: + required: true + - type: input + id: page + attributes: + label: Page + description: Which page is this about? A URL is ideal. + placeholder: https://computer.musicthing.co.uk/ + - type: textarea + id: details + attributes: + label: Details + description: What happened, or what should change? Include screenshots if they help. + validations: + required: true diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 40c7d348e..f6ea1d75a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -18,6 +18,11 @@ jobs: build: if: github.event.repository.fork != true || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + issues: write env: # Fork previews retain their own github.io project URL. The upstream # production build emits canonical web-editor links on the custom domain. @@ -45,6 +50,13 @@ jobs: run: | npm test --prefix tools/sitegen + - name: Synchronize program-card issue labels + if: github.repository == 'TomWhitwell/Workshop_Computer' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: npm run sync-card-labels --prefix tools/sitegen + - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/package.json b/package.json index f3a3487ec..11f83a0a9 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "validate-info": "npm --prefix tools/sitegen run validate-info", "check-curation": "npm --prefix tools/sitegen run check-curation", "sync-curation": "npm --prefix tools/sitegen run sync-curation", + "sync-card-labels": "npm --prefix tools/sitegen run sync-card-labels", "setup-panel-renderer": "npm --prefix tools/sitegen run setup-panel-renderer", "hooks:install": "node tools/sitegen/src/validate/installHooks.js", "validate-staged": "node tools/sitegen/src/validate/validateStaged.js", diff --git a/tools/sitegen/package.json b/tools/sitegen/package.json index 2cc267c44..fb11f23b8 100644 --- a/tools/sitegen/package.json +++ b/tools/sitegen/package.json @@ -10,6 +10,7 @@ "validate-info": "node ./src/validate/cli.js", "check-curation": "node ./src/curation/cli.js check", "sync-curation": "node ./src/curation/cli.js sync", + "sync-card-labels": "node ./src/github/syncCardLabels.js", "setup-panel-renderer": "playwright install --with-deps chromium", "test": "npm run build && node --test" }, diff --git a/tools/sitegen/src/curation/cli.js b/tools/sitegen/src/curation/cli.js index 0c4f799e2..a05317c87 100644 --- a/tools/sitegen/src/curation/cli.js +++ b/tools/sitegen/src/curation/cli.js @@ -23,7 +23,7 @@ function readDocument(file) { return document; } -function releaseCards() { +export function releaseCards() { return fs.readdirSync(RELEASES_DIR, { withFileTypes: true }) .filter(entry => entry.isDirectory() && fs.existsSync(path.join(RELEASES_DIR, entry.name, 'info.yaml'))) .map(entry => { diff --git a/tools/sitegen/src/github/syncCardLabels.js b/tools/sitegen/src/github/syncCardLabels.js new file mode 100644 index 000000000..ad054031f --- /dev/null +++ b/tools/sitegen/src/github/syncCardLabels.js @@ -0,0 +1,87 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { releaseCards } from '../curation/cli.js'; +import { + NEW_CARD_LABEL_COLOR, + UPSTREAM_REPO, + cardIssueLabelsFromCards, +} from '../render/githubIssue.js'; + +const DEFAULT_DESCRIPTION = 'Program card'; + +export function missingCardIssueLabels(desired, existing) { + const have = new Set((existing || []).map(name => String(name))); + return (desired || []).filter(label => !have.has(label)); +} + +function gh(args, { token } = {}) { + const env = { ...process.env }; + if (token) { + env.GH_TOKEN = token; + env.GITHUB_TOKEN = token; + } + const result = spawnSync('gh', args, { encoding: 'utf8', env }); + if (result.error) throw result.error; + if (result.status !== 0) { + const detail = (result.stderr || result.stdout || '').trim() || `gh exited ${result.status}`; + throw new Error(detail); + } + return result.stdout; +} + +function listExistingLabels(repo, token) { + const stdout = gh(['label', 'list', '--repo', repo, '--json', 'name', '--limit', '1000'], { token }); + const parsed = JSON.parse(stdout || '[]'); + return parsed.map(entry => entry.name).filter(Boolean); +} + +function createLabel(repo, name, token) { + gh([ + 'label', 'create', name, + '--repo', repo, + '--color', NEW_CARD_LABEL_COLOR, + '--description', DEFAULT_DESCRIPTION, + ], { token }); +} + +export function syncCardIssueLabels({ + cards = releaseCards(), + repo = process.env.GH_REPO || UPSTREAM_REPO, + token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '', + dryRun = process.argv.includes('--dry-run') || !token, + log = console, +} = {}) { + const desired = cardIssueLabelsFromCards(cards); + if (dryRun) { + log.log(`Would ensure ${desired.length} program-card label(s) on ${repo} (dry run).`); + for (const label of desired) log.log(` ${label}`); + return { desired, created: [], skipped: desired, dryRun: true }; + } + + const existing = listExistingLabels(repo, token); + const missing = missingCardIssueLabels(desired, existing); + const created = []; + for (const label of missing) { + try { + createLabel(repo, label, token); + created.push(label); + log.log(`Created label: ${label}`); + } catch (error) { + if (/already exists/i.test(error.message)) continue; + throw new Error(`Failed to create label "${label}": ${error.message}`); + } + } + if (!created.length) log.log(`Program-card labels are already synchronized (${desired.length} labels).`); + else log.log(`Created ${created.length} program-card label(s).`); + return { desired, created, skipped: missing.filter(label => !created.includes(label)), dryRun: false }; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + syncCardIssueLabels(); + } catch (error) { + console.error(`error: ${error.message}`); + process.exitCode = 1; + } +} diff --git a/tools/sitegen/src/render/cardPage.js b/tools/sitegen/src/render/cardPage.js index 36ab8b2df..001dac5ad 100644 --- a/tools/sitegen/src/render/cardPage.js +++ b/tools/sitegen/src/render/cardPage.js @@ -9,6 +9,7 @@ import { panelPositions } from './panelPositions.js'; import { renderMarkdownBlock, renderMarkdownInline, sanitizeAuthoredHtml } from '../utils/markdown.js'; import { instagramEmbedHtml } from '../utils/instagram.js'; +import { cardFeedbackHostLabel, cardFeedbackUrl } from './githubIssue.js'; import { externalLinkArrow } from './icons.js'; const DEFAULT_DISCUSSION = 'https://discord.com/channels/1210238368898879569/1484219323039092938'; @@ -417,6 +418,8 @@ export function renderCardArticle({ card, panelImg, yamlUrl, uf2Url, extraDocs = const hasPanel = !basic && hasPanelDefinition(card); const panelRail = hasPanel ? renderPanelRail(card, panelImg) : ''; const discussionUrl = metadata.discussion_url || DEFAULT_DISCUSSION; + const feedbackUrl = cardFeedbackUrl(card); + const feedbackHost = cardFeedbackHostLabel(card); const firstVideo = Array.isArray(card.videos) && card.videos[0]; const sourceLinkUrl = metadata.repository || sourceUrl; const sourceLinkLabel = metadata.repository ? 'Upstream repository' : 'Release folder in the Workshop Computer repo'; @@ -468,7 +471,7 @@ export function renderCardArticle({ card, panelImg, yamlUrl, uf2Url, extraDocs = ${basic || !memoryMarkup ? '' : `
${memoryMarkup}
`}
${downloadActions}${editorAction}
- + `; @@ -502,6 +505,7 @@ export function renderCardArticle({ card, panelImg, yamlUrl, uf2Url, extraDocs = ${readmeUrl ? `
Read more
README in the Workshop Computer repo
` : ''} ${sourceLinkUrl ? `
Source
${sourceLinkLabel}
` : ''}
Support
Ask questions, contact the designer, or share feedback
+
Feedback
Report an issue on ${esc(feedbackHost)}
${notesMarkup} ${dataSources} diff --git a/tools/sitegen/src/render/githubIssue.js b/tools/sitegen/src/render/githubIssue.js new file mode 100644 index 000000000..d90f36948 --- /dev/null +++ b/tools/sitegen/src/render/githubIssue.js @@ -0,0 +1,105 @@ +export const UPSTREAM_REPO = 'TomWhitwell/Workshop_Computer'; +export const UPSTREAM_ISSUES = `https://github.com/${UPSTREAM_REPO}`; +export const GITHUB_LABEL_MAX = 50; +export const NEW_CARD_LABEL_COLOR = 'c5def5'; +export const SKIP_CARD_LABEL_IDS = new Set(['02_comingsoon', '77_Placeholder']); + +export function cardFolderNumber(id) { + return String(id || '').split('_')[0].trim(); +} + +export function cardIssueLabel({ id, title } = {}) { + const number = cardFolderNumber(id); + const name = String(title || '').replace(/\s+/g, ' ').trim(); + let label = !number ? name + : !name ? number + : (name === number || name.startsWith(`${number} `)) ? name + : `${number} ${name}`; + label = label.replace(/[\u0000-\u001f]/g, '').trim(); + if (label.length > GITHUB_LABEL_MAX) label = label.slice(0, GITHUB_LABEL_MAX).trimEnd(); + return label; +} + +export function cardIssueLabelsFromCards(cards) { + const labels = []; + const seen = new Set(); + for (const card of Array.isArray(cards) ? cards : []) { + if (!card || SKIP_CARD_LABEL_IDS.has(card.id)) continue; + const label = cardIssueLabel(card); + if (!label || seen.has(label)) continue; + seen.add(label); + labels.push(label); + } + return labels; +} + +export function parseForgeRepository(raw) { + const text = String(raw || '').trim(); + if (!text) return null; + let url; + try { + url = new URL(text); + } catch { + return null; + } + if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; + const host = url.hostname.replace(/^www\./i, '').toLowerCase(); + const parts = url.pathname.replace(/\/+$/, '').split('/').filter(Boolean); + if (parts.length < 2) return null; + const owner = parts[0]; + const repo = parts[1].replace(/\.git$/i, ''); + if (!owner || !repo) return null; + if (/^(orgs|settings|login|marketplace|topics|features)$/i.test(owner)) return null; + const kind = host === 'github.com' ? 'github' + : host === 'gitlab.com' ? 'gitlab' + : 'forgejo'; + return { host, owner, repo, origin: `${url.protocol}//${url.host}`, kind }; +} + +export function isCanonicalWorkshopRepo(parsed) { + return parsed?.kind === 'github' + && String(parsed.owner).toLowerCase() === 'tomwhitwell' + && String(parsed.repo).toLowerCase() === 'workshop_computer'; +} + +export function cardFeedbackHostLabel(card) { + const parsed = parseForgeRepository(card?.metadata?.repository); + if (parsed && !isCanonicalWorkshopRepo(parsed)) { + if (parsed.host === 'github.com') return 'GitHub'; + if (parsed.host === 'codeberg.org') return 'Codeberg'; + if (parsed.host === 'gitlab.com') return 'GitLab'; + return parsed.host; + } + return 'GitHub'; +} + +export function websiteFeedbackUrl() { + return `${UPSTREAM_ISSUES}/issues/new?template=website_feedback.yml`; +} + +function catalogueFeedbackUrl(card) { + const label = cardIssueLabel(card); + const params = new URLSearchParams({ + template: 'card_feedback.yml', + labels: label, + card: label, + }); + return `${UPSTREAM_ISSUES}/issues/new?${params}`; +} + +function externalIssuesUrl(parsed, card) { + const title = cardIssueLabel(card); + if (parsed.kind === 'gitlab') { + const params = new URLSearchParams({ 'issue[title]': title ? `${title}: ` : '' }); + return `${parsed.origin}/${parsed.owner}/${parsed.repo}/-/issues/new?${params}`; + } + const base = `${parsed.origin}/${parsed.owner}/${parsed.repo}/issues/new`; + if (!title) return base; + return `${base}?${new URLSearchParams({ title: `${title}: ` })}`; +} + +export function cardFeedbackUrl(card) { + const parsed = parseForgeRepository(card?.metadata?.repository); + if (parsed && !isCanonicalWorkshopRepo(parsed)) return externalIssuesUrl(parsed, card); + return catalogueFeedbackUrl(card); +} diff --git a/tools/sitegen/src/render/layout.js b/tools/sitegen/src/render/layout.js index d86358194..9a7a159dc 100644 --- a/tools/sitegen/src/render/layout.js +++ b/tools/sitegen/src/render/layout.js @@ -1,4 +1,5 @@ import { applyContentSecurityPolicy, CSP_PLACEHOLDER } from './csp.js'; +import { websiteFeedbackUrl } from './githubIssue.js'; import { externalLinkArrow } from './icons.js'; import { renderSocialMeta } from './socialMeta.js'; @@ -51,7 +52,7 @@ export function renderLayout({ title, content, relativeRoot = '.', legacyRedirec

Music Thing Modular

diff --git a/tools/sitegen/test/githubIssue.test.js b/tools/sitegen/test/githubIssue.test.js new file mode 100644 index 000000000..6b943d6aa --- /dev/null +++ b/tools/sitegen/test/githubIssue.test.js @@ -0,0 +1,98 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { releaseCards } from '../src/curation/cli.js'; +import { missingCardIssueLabels } from '../src/github/syncCardLabels.js'; +import { + GITHUB_LABEL_MAX, + cardFeedbackHostLabel, + cardFeedbackUrl, + cardIssueLabel, + cardIssueLabelsFromCards, +} from '../src/render/githubIssue.js'; + +test('cardIssueLabel follows the existing GitHub number-plus-name pattern', () => { + assert.equal(cardIssueLabel({ id: '00_Simple_MIDI', title: 'Simple MIDI' }), '00 Simple MIDI'); + assert.equal(cardIssueLabel({ id: '03_Turing_Machine', title: 'Turing Machine' }), '03 Turing Machine'); + assert.equal(cardIssueLabel({ id: '20_reverb', title: 'Reverb+' }), '20 Reverb+'); + assert.equal(cardIssueLabel({ id: '41_blackbird', title: 'Blackbird' }), '41 Blackbird'); + assert.equal(cardIssueLabel({ id: '433_sense_of_space', title: '433 Sense of Space' }), '433 Sense of Space'); +}); + +test('cardIssueLabel truncates to GitHub’s 50-character limit', () => { + const title = 'A very long program card title that must be truncated for GitHub'; + const label = cardIssueLabel({ id: '355_long', title }); + assert.equal(label.length, GITHUB_LABEL_MAX); + assert.equal(label.startsWith('355 A very long'), true); +}); + +test('cardIssueLabelsFromCards skips placeholders and dedupes', () => { + assert.deepEqual(cardIssueLabelsFromCards([ + { id: '02_comingsoon', title: 'Coming Soon' }, + { id: '41_blackbird', title: 'Blackbird' }, + { id: '77_Placeholder', title: 'Placeholder' }, + { id: '41_blackbird', title: 'Blackbird' }, + ]), ['41 Blackbird']); +}); + +test('cardFeedbackUrl uses the catalogue template for local cards', () => { + const local = { id: '41_blackbird', title: 'Blackbird' }; + const inMonorepo = { + id: '60_markov', + title: 'Markov', + metadata: { repository: 'https://github.com/TomWhitwell/Workshop_Computer/tree/main/releases/60_markov' }, + }; + for (const card of [local, inMonorepo]) { + const url = new URL(cardFeedbackUrl(card)); + assert.equal(url.origin + url.pathname, 'https://github.com/TomWhitwell/Workshop_Computer/issues/new'); + assert.equal(url.searchParams.get('template'), 'card_feedback.yml'); + assert.equal(url.searchParams.get('card'), cardIssueLabel(card)); + } +}); + +test('cardFeedbackUrl sends offsite cards to their own issue tracker', () => { + const githubCard = { + id: '61_ZX_Spectrum', + title: 'ZX', + metadata: { repository: 'https://github.com/uglifruit/WorkshopZX' }, + }; + assert.equal(cardFeedbackUrl(githubCard), 'https://github.com/uglifruit/WorkshopZX/issues/new?title=61+ZX%3A+'); + assert.equal(cardFeedbackHostLabel(githubCard), 'GitHub'); + + const githubTree = cardFeedbackUrl({ + id: '53_glitter', + title: 'Glitter', + metadata: { repository: 'https://github.com/sdrjones/mtws/tree/main/53_glitter' }, + }); + assert.equal(githubTree, 'https://github.com/sdrjones/mtws/issues/new?title=53+Glitter%3A+'); + + const codebergCard = { + id: '64_voices_of_sid', + title: 'Voices of SID', + metadata: { repository: 'https://codeberg.org/johantv/voices-of-sid' }, + }; + assert.equal(cardFeedbackUrl(codebergCard), 'https://codeberg.org/johantv/voices-of-sid/issues/new?title=64+Voices+of+SID%3A+'); + assert.equal(cardFeedbackHostLabel(codebergCard), 'Codeberg'); + + const codebergPath = cardFeedbackUrl({ + id: '42_backyard_rain', + title: 'Backyard Rain', + metadata: { repository: 'https://codeberg.org/briandorsey/mtmws_cards/src/branch/main/backyard_rain' }, + }); + assert.equal(codebergPath, 'https://codeberg.org/briandorsey/mtmws_cards/issues/new?title=42+Backyard+Rain%3A+'); +}); + +test('missingCardIssueLabels only creates names that are not already on the repo', () => { + assert.deepEqual( + missingCardIssueLabels(['41 Blackbird', '00 Simple MIDI', '97 Alloy'], ['41 Blackbird', 'Website']), + ['00 Simple MIDI', '97 Alloy'], + ); +}); + +test('catalogue cards produce the labels already used on GitHub', () => { + const labels = cardIssueLabelsFromCards(releaseCards()); + for (const expected of ['00 Simple MIDI', '03 Turing Machine', '20 Reverb+', '41 Blackbird', '88 Blank', '433 Sense of Space']) { + assert.equal(labels.includes(expected), true, `expected ${expected}`); + } + assert.equal(labels.includes('433 433 Sense of Space'), false); + assert.equal(labels.includes('02 Coming Soon'), false); +}); diff --git a/tools/sitegen/test/render.test.js b/tools/sitegen/test/render.test.js index 4b92c866e..10072cb7e 100644 --- a/tools/sitegen/test/render.test.js +++ b/tools/sitegen/test/render.test.js @@ -4,6 +4,7 @@ import { renderCardArticle, renderPanelArtwork, renderReadmeAndDocs } from '../s import { orderFlairShelfCards, renderArchive, renderShelf, renderTile } from '../src/render/discovery.js'; import { renderLayout } from '../src/render/layout.js'; import { renderAuthorPage } from '../src/render/authorPage.js'; +import { cardFeedbackUrl, websiteFeedbackUrl } from '../src/render/githubIssue.js'; function card(extra = {}) { return { @@ -431,4 +432,46 @@ test('basic author mode exposes live-preview web editor metadata', () => { assert.match(preview, /data-add-optional="Editor"/); assert.match(preview, /data-field="Editor"/); assert.match(preview, /data-field="web-entry"/); +}); + +test('layout footer links to the website feedback issue template', () => { + const html = renderLayout({ title: 'Safe', content: '

Content

' }); + assert.match(html, /Website feedback<\/a>/); + assert.equal( + websiteFeedbackUrl(), + 'https://github.com/TomWhitwell/Workshop_Computer/issues/new?template=website_feedback.yml', + ); +}); + +test('card pages link to a prefilled program-card issue template', () => { + const blackbird = card({ + id: '41_blackbird', + title: 'Blackbird', + slug: '41-blackbird', + release: '41 / 1.1', + }); + const html = renderCardArticle({ card: blackbird, panelImg: 'panel.svg', yamlUrl: 'source.yaml' }); + const href = cardFeedbackUrl(blackbird).replace(/&/g, '&'); + const escaped = href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + assert.match(href, /template=card_feedback\.yml/); + assert.match(href, /labels=41\+Blackbird/); + assert.match(href, /card=41\+Blackbird/); + assert.match(html, new RegExp(`href="${escaped}">Send feedback`)); + assert.match(html, new RegExp(`
Feedback
Report an issue on GitHub
`)); +}); + +test('offsite cards send feedback to the upstream forge, not this catalogue', () => { + const voices = card({ + id: '64_voices_of_sid', + title: 'Voices of SID', + slug: '64-voices-of-sid', + metadata: { repository: 'https://codeberg.org/johantv/voices-of-sid' }, + }); + const html = renderCardArticle({ card: voices, panelImg: 'panel.svg', yamlUrl: 'source.yaml' }); + const href = cardFeedbackUrl(voices).replace(/&/g, '&'); + const escaped = href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + assert.match(href, /codeberg\.org\/johantv\/voices-of-sid\/issues\/new/); + assert.doesNotMatch(html, /template=card_feedback\.yml/); + assert.match(html, new RegExp(`href="${escaped}">Send feedback`)); + assert.match(html, new RegExp(`
Feedback
Report an issue on Codeberg
`)); }); \ No newline at end of file From 815a7ebff7b315165a0b9b7f9eca440be51e4ab4 Mon Sep 17 00:00:00 2001 From: Dune Desormeaux Date: Sat, 22 Aug 2026 20:10:23 +0000 Subject: [PATCH 2/2] fold label sync into curation sync (rename to metadata sync) --- .github/workflows/pages.yml | 19 ++++--------------- .github/workflows/sync-curation.yml | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index f6ea1d75a..c165d8137 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,8 +1,9 @@ name: Build and Deploy GitHub Pages on: - push: - branches: [ "main" ] + workflow_run: + workflows: ["Synchronize repository metadata"] + types: [completed] workflow_dispatch: permissions: @@ -16,13 +17,8 @@ concurrency: jobs: build: - if: github.event.repository.fork != true || github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' || (github.event.repository.fork != true && github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest - permissions: - contents: read - pages: write - id-token: write - issues: write env: # Fork previews retain their own github.io project URL. The upstream # production build emits canonical web-editor links on the custom domain. @@ -50,13 +46,6 @@ jobs: run: | npm test --prefix tools/sitegen - - name: Synchronize program-card issue labels - if: github.repository == 'TomWhitwell/Workshop_Computer' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: npm run sync-card-labels --prefix tools/sitegen - - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/.github/workflows/sync-curation.yml b/.github/workflows/sync-curation.yml index 5756cf71a..3ec4bd8a9 100644 --- a/.github/workflows/sync-curation.yml +++ b/.github/workflows/sync-curation.yml @@ -1,4 +1,4 @@ -name: Synchronize site curation +name: Synchronize repository metadata on: push: @@ -7,14 +7,15 @@ on: workflow_dispatch: concurrency: - group: sync-site-curation + group: sync-repository-metadata cancel-in-progress: true permissions: contents: write + issues: write jobs: - sync-curation: + sync-metadata: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -45,4 +46,11 @@ jobs: git commit -m 'Synchronize site curation' git push env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Synchronize program-card issue labels + if: github.repository == 'TomWhitwell/Workshop_Computer' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: npm run sync-card-labels \ No newline at end of file