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..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,7 +17,7 @@ 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 env: # Fork previews retain their own github.io project URL. The upstream 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 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 ? '' : `
`} 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(`