From 7d6ac4bc23a89e026c9b2376bcdf327603f9a4aa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 19 May 2026 11:08:22 +0000
Subject: [PATCH 1/6] feat: add Antora downloads dropdown and prostep logo
Agent-Logs-Url: https://github.com/PMSFIT/smartse-template/sessions/464d05ce-653d-4baf-a4b2-342b51f17b2f
Co-authored-by: pmai <91502+pmai@users.noreply.github.com>
---
.github/workflows/docs.yml | 183 ++++++++++++++++++
antora-playbook.yml | 1 +
supplemental-ui/css/custom.css | 90 +++++++++
.../downloads/downloads-manifest.json | 22 +++
supplemental-ui/downloads/generated/.gitkeep | 0
supplemental-ui/img/prostep_logo.svg | 90 +++++++++
supplemental-ui/js/downloads.js | 164 ++++++++++++++++
supplemental-ui/partials/footer-scripts.hbs | 6 +
supplemental-ui/partials/head-styles.hbs | 2 +
supplemental-ui/partials/header-content.hbs | 41 ++++
10 files changed, 599 insertions(+)
create mode 100644 supplemental-ui/css/custom.css
create mode 100644 supplemental-ui/downloads/downloads-manifest.json
create mode 100644 supplemental-ui/downloads/generated/.gitkeep
create mode 100644 supplemental-ui/img/prostep_logo.svg
create mode 100644 supplemental-ui/js/downloads.js
create mode 100644 supplemental-ui/partials/footer-scripts.hbs
create mode 100644 supplemental-ui/partials/head-styles.hbs
create mode 100644 supplemental-ui/partials/header-content.hbs
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 9695178..ef9c16e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [ main, develop ]
+permissions:
+ actions: read
+ contents: write
+
jobs:
docs:
runs-on: ubuntu-latest
@@ -21,6 +25,185 @@ jobs:
- name: Install Antora
run: npm install -g @antora/cli@3.1 @antora/site-generator@3.1
+ - name: Generate downloads manifest and ZIPs
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ node <<'EOF'
+ const fs = require('fs/promises')
+ const path = require('path')
+
+ const ownerRepo = process.env.GITHUB_REPOSITORY || ''
+ const [owner, repo] = ownerRepo.split('/')
+ const token = process.env.GITHUB_TOKEN
+ const apiUrl = process.env.GITHUB_API_URL || 'https://api.github.com'
+ const eventName = process.env.GITHUB_EVENT_NAME
+ const refName = process.env.GITHUB_REF_NAME
+ const sha = process.env.GITHUB_SHA
+ const workspace = process.env.GITHUB_WORKSPACE
+ const eventPath = process.env.GITHUB_EVENT_PATH
+ const event = JSON.parse(await fs.readFile(eventPath, 'utf8'))
+ const branch = eventName === 'pull_request' ? event.pull_request?.head?.ref : refName
+ const prNumber = event.pull_request?.number ?? null
+ const outputRoot = path.join(workspace, 'supplemental-ui', 'downloads')
+ const generatedRoot = path.join(outputRoot, 'generated')
+
+ await fs.mkdir(generatedRoot, { recursive: true })
+ for (const entry of await fs.readdir(generatedRoot)) {
+ if (entry !== '.gitkeep') {
+ await fs.rm(path.join(generatedRoot, entry), { force: true, recursive: true })
+ }
+ }
+
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+ const sanitize = (value) => value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
+ const platformLabel = (value) => value
+ .replace(/-latest$/i, '')
+ .split('-')
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(' ')
+
+ const apiFetch = async (url, options = {}) => {
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ Accept: 'application/vnd.github+json',
+ Authorization: `Bearer ${token}`,
+ 'X-GitHub-Api-Version': '2022-11-28',
+ ...(options.headers || {}),
+ },
+ })
+ if (!response.ok) {
+ throw new Error(`${url} -> ${response.status} ${response.statusText}`)
+ }
+ return response
+ }
+
+ const listRuns = async (workflowFile) => {
+ const params = new URLSearchParams({
+ branch,
+ event: eventName,
+ per_page: '50',
+ })
+ const response = await apiFetch(`${apiUrl}/repos/${owner}/${repo}/actions/workflows/${workflowFile}/runs?${params}`)
+ const payload = await response.json()
+ return payload.workflow_runs || []
+ }
+
+ const matchesContext = (run) => {
+ if (!run) return false
+ if (run.head_sha === sha) return true
+ if (eventName === 'pull_request' && prNumber) {
+ return Array.isArray(run.pull_requests) && run.pull_requests.some((pullRequest) => pullRequest.number === prNumber)
+ }
+ return run.head_branch === branch
+ }
+
+ const findContextRun = async (workflowFile, title) => {
+ let fallbackRun = null
+ for (let attempt = 0; attempt < 30; attempt += 1) {
+ const runs = await listRuns(workflowFile)
+ const contextRuns = runs.filter(matchesContext)
+ const contextRun = contextRuns.find((run) => run.head_sha === sha) || contextRuns[0]
+
+ if (!fallbackRun) {
+ fallbackRun = contextRuns.find((run) => run.status === 'completed' && run.conclusion === 'success') || null
+ }
+
+ if (contextRun) {
+ if (contextRun.status === 'completed') {
+ if (contextRun.conclusion === 'success') return contextRun
+ console.warn(`${title} run for ${sha} completed with ${contextRun.conclusion}`)
+ return null
+ }
+ }
+
+ if (attempt < 29) {
+ console.log(`Waiting for ${title} workflow artifacts for ${branch} (${attempt + 1}/30)...`)
+ await sleep(20000)
+ }
+ }
+
+ if (fallbackRun) {
+ console.warn(`Using latest successful ${title} run for the current ${eventName === 'pull_request' ? 'pull request' : 'branch'} context.`)
+ return fallbackRun
+ }
+
+ console.warn(`Timed out waiting for ${title} workflow run on ${branch}`)
+ return null
+ }
+
+ const listArtifacts = async (runId) => {
+ const response = await apiFetch(`${apiUrl}/repos/${owner}/${repo}/actions/runs/${runId}/artifacts?per_page=100`)
+ const payload = await response.json()
+ return payload.artifacts || []
+ }
+
+ const downloadArtifact = async (artifact, fileName) => {
+ const response = await apiFetch(artifact.archive_download_url)
+ const buffer = Buffer.from(await response.arrayBuffer())
+ await fs.writeFile(path.join(generatedRoot, fileName), buffer)
+ }
+
+ const sectionSpecs = [
+ { id: 'build', title: 'Build', workflow: 'build.yml', prefix: 'build-' },
+ { id: 'tests', title: 'Tests', workflow: 'test.yml', prefix: 'test-results-' },
+ ]
+
+ const sections = []
+ for (const spec of sectionSpecs) {
+ const run = await findContextRun(spec.workflow, spec.title)
+ if (!run) {
+ sections.push({ id: spec.id, title: spec.title, items: [] })
+ continue
+ }
+
+ const artifacts = (await listArtifacts(run.id))
+ .filter((artifact) => !artifact.expired && artifact.name.startsWith(spec.prefix))
+ .sort((left, right) => left.name.localeCompare(right.name))
+
+ const items = []
+ for (const artifact of artifacts) {
+ const fileName = `${sanitize(spec.id)}-${sanitize(artifact.name)}-run${run.run_number}.zip`
+ await downloadArtifact(artifact, fileName)
+ items.push({
+ id: artifact.id,
+ name: artifact.name,
+ label: artifact.name,
+ platform: platformLabel(artifact.name.slice(spec.prefix.length)),
+ run_number: run.run_number,
+ created_at: artifact.created_at,
+ size_in_bytes: artifact.size_in_bytes,
+ expired: artifact.expired,
+ download: `${artifact.name}.zip`,
+ url: `generated/${fileName}`,
+ })
+ }
+
+ sections.push({
+ id: spec.id,
+ title: spec.title,
+ workflow_run_id: run.id,
+ workflow_run_number: run.run_number,
+ items,
+ })
+ }
+
+ const manifest = {
+ generated_at: new Date().toISOString(),
+ context: {
+ repository: ownerRepo,
+ event: eventName,
+ ref: branch,
+ sha,
+ pr_number: prNumber,
+ },
+ sections,
+ }
+
+ await fs.writeFile(path.join(outputRoot, 'downloads-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
+ EOF
+
- name: Build Documentation
run: antora antora-playbook.yml
diff --git a/antora-playbook.yml b/antora-playbook.yml
index 3b8f8f8..3a465dd 100644
--- a/antora-playbook.yml
+++ b/antora-playbook.yml
@@ -13,6 +13,7 @@ ui:
bundle:
url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
snapshot: true
+ supplemental_files: ./supplemental-ui
output:
dir: ./build/site
diff --git a/supplemental-ui/css/custom.css b/supplemental-ui/css/custom.css
new file mode 100644
index 0000000..f1c817f
--- /dev/null
+++ b/supplemental-ui/css/custom.css
@@ -0,0 +1,90 @@
+.prostep-logo-link {
+ padding-right: 0.25rem;
+}
+
+.prostep-logo {
+ display: block;
+ height: 2rem;
+ width: auto;
+}
+
+.downloads-toggle {
+ appearance: none;
+ background: none;
+ border: 0;
+ color: inherit;
+ cursor: pointer;
+ font: inherit;
+ padding: 0;
+}
+
+.downloads-dropdown-menu {
+ min-width: 20rem;
+ max-width: 24rem;
+}
+
+.downloads-status,
+.downloads-section-title,
+.downloads-empty {
+ white-space: normal;
+}
+
+.downloads-status.is-error {
+ color: #b42318;
+}
+
+.downloads-section-title {
+ color: #6b7280;
+ cursor: default;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.downloads-artifact {
+ align-items: flex-start;
+ display: block;
+ line-height: 1.35;
+ white-space: normal;
+}
+
+.downloads-artifact-label,
+.downloads-artifact-meta {
+ display: block;
+}
+
+.downloads-artifact-label {
+ font-weight: 600;
+}
+
+.downloads-artifact-meta {
+ color: #6b7280;
+ font-size: 0.85em;
+ margin-top: 0.15rem;
+}
+
+.navbar-item.has-dropdown.is-active .navbar-dropdown {
+ display: block;
+}
+
+.navbar-item.github svg {
+ display: block;
+}
+
+.navbar-item.is-disabled {
+ cursor: default;
+ opacity: 0.8;
+ pointer-events: none;
+}
+
+@media screen and (max-width: 1023px) {
+ .prostep-logo {
+ height: 1.75rem;
+ }
+
+ .downloads-dropdown-menu {
+ max-width: none;
+ min-width: 100%;
+ }
+}
diff --git a/supplemental-ui/downloads/downloads-manifest.json b/supplemental-ui/downloads/downloads-manifest.json
new file mode 100644
index 0000000..72d129f
--- /dev/null
+++ b/supplemental-ui/downloads/downloads-manifest.json
@@ -0,0 +1,22 @@
+{
+ "generated_at": null,
+ "context": {
+ "repository": "PMSFIT/smartse-template",
+ "event": null,
+ "ref": null,
+ "sha": null,
+ "pr_number": null
+ },
+ "sections": [
+ {
+ "id": "build",
+ "title": "Build",
+ "items": []
+ },
+ {
+ "id": "tests",
+ "title": "Tests",
+ "items": []
+ }
+ ]
+}
diff --git a/supplemental-ui/downloads/generated/.gitkeep b/supplemental-ui/downloads/generated/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/supplemental-ui/img/prostep_logo.svg b/supplemental-ui/img/prostep_logo.svg
new file mode 100644
index 0000000..2dbb6f1
--- /dev/null
+++ b/supplemental-ui/img/prostep_logo.svg
@@ -0,0 +1,90 @@
+
+
+
+
diff --git a/supplemental-ui/js/downloads.js b/supplemental-ui/js/downloads.js
new file mode 100644
index 0000000..a5e190c
--- /dev/null
+++ b/supplemental-ui/js/downloads.js
@@ -0,0 +1,164 @@
+(() => {
+ const dropdowns = document.querySelectorAll('[data-downloads-dropdown]')
+ if (!dropdowns.length) return
+
+ const formatDate = (value) => {
+ if (!value) return ''
+ const date = new Date(value)
+ if (Number.isNaN(date.getTime())) return ''
+ return new Intl.DateTimeFormat(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ }).format(date)
+ }
+
+ const renderMessage = (menu, message, extraClass = '') => {
+ const node = document.createElement('div')
+ node.className = `navbar-item is-disabled downloads-status${extraClass ? ` ${extraClass}` : ''}`
+ node.textContent = message
+ menu.replaceChildren(node)
+ }
+
+ const createArtifactLink = (item) => {
+ const link = document.createElement('a')
+ link.className = 'navbar-item downloads-artifact'
+ link.href = item.url
+ link.setAttribute('download', item.download || '')
+
+ const label = document.createElement('span')
+ label.className = 'downloads-artifact-label'
+ label.textContent = item.label || item.name || 'Download artifact'
+ link.appendChild(label)
+
+ const metaParts = []
+ if (item.platform) metaParts.push(item.platform)
+ if (item.run_number) metaParts.push(`Run #${item.run_number}`)
+ const createdAt = formatDate(item.created_at)
+ if (createdAt) metaParts.push(createdAt)
+ if (metaParts.length) {
+ const meta = document.createElement('span')
+ meta.className = 'downloads-artifact-meta'
+ meta.textContent = metaParts.join(' • ')
+ link.appendChild(meta)
+ }
+
+ return link
+ }
+
+ const renderManifest = (menu, manifest) => {
+ const sections = Array.isArray(manifest?.sections) ? manifest.sections : []
+ const fragment = document.createDocumentFragment()
+ let hasArtifacts = false
+
+ sections.forEach((section) => {
+ const items = Array.isArray(section?.items)
+ ? section.items.filter((item) => item?.url && !item?.expired)
+ : []
+ if (!items.length) return
+
+ if (hasArtifacts) {
+ const divider = document.createElement('hr')
+ divider.className = 'navbar-divider'
+ fragment.appendChild(divider)
+ }
+
+ const title = document.createElement('div')
+ title.className = 'navbar-item is-disabled downloads-section-title'
+ title.textContent = section.title || 'Downloads'
+ fragment.appendChild(title)
+
+ items.forEach((item) => {
+ fragment.appendChild(createArtifactLink(item))
+ })
+
+ hasArtifacts = true
+ })
+
+ if (!hasArtifacts) {
+ renderMessage(menu, 'No downloads available for this branch or pull request.')
+ return
+ }
+
+ menu.replaceChildren(fragment)
+ }
+
+ const closeDropdown = (container, toggle, focusToggle = false) => {
+ container.classList.remove('is-active')
+ toggle.setAttribute('aria-expanded', 'false')
+ if (focusToggle) toggle.focus()
+ }
+
+ const openDropdown = (container, toggle, focusFirst = false) => {
+ container.classList.add('is-active')
+ toggle.setAttribute('aria-expanded', 'true')
+ if (focusFirst) {
+ const firstLink = container.querySelector('.downloads-artifact')
+ if (firstLink) firstLink.focus()
+ }
+ }
+
+ dropdowns.forEach((container) => {
+ const toggle = container.querySelector('.downloads-toggle')
+ const menu = container.querySelector('[data-downloads-menu]')
+ const manifestUrl = container.dataset.manifestUrl
+ if (!toggle || !menu || !manifestUrl) return
+
+ const absoluteManifestUrl = new URL(manifestUrl, window.location.href)
+
+ toggle.addEventListener('click', () => {
+ if (container.classList.contains('is-active')) {
+ closeDropdown(container, toggle)
+ } else {
+ openDropdown(container, toggle)
+ }
+ })
+
+ toggle.addEventListener('keydown', (event) => {
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ openDropdown(container, toggle, true)
+ } else if (event.key === 'Escape') {
+ event.preventDefault()
+ closeDropdown(container, toggle)
+ }
+ })
+
+ menu.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape') {
+ event.preventDefault()
+ closeDropdown(container, toggle, true)
+ }
+ })
+
+ menu.addEventListener('click', (event) => {
+ if (event.target.closest('a')) closeDropdown(container, toggle)
+ })
+
+ document.addEventListener('click', (event) => {
+ if (!container.contains(event.target)) closeDropdown(container, toggle)
+ })
+
+ fetch(absoluteManifestUrl, {
+ headers: { Accept: 'application/json' },
+ cache: 'no-store',
+ })
+ .then((response) => {
+ if (!response.ok) throw new Error(`Manifest request failed with ${response.status}`)
+ return response.json()
+ })
+ .then((manifest) => {
+ const normalizedSections = (manifest.sections || []).map((section) => ({
+ ...section,
+ items: (section.items || []).map((item) => ({
+ ...item,
+ url: new URL(item.url, absoluteManifestUrl).toString(),
+ })),
+ }))
+ renderManifest(menu, { ...manifest, sections: normalizedSections })
+ })
+ .catch(() => {
+ renderMessage(menu, 'Downloads are currently unavailable.', 'is-error')
+ })
+ })
+})()
diff --git a/supplemental-ui/partials/footer-scripts.hbs b/supplemental-ui/partials/footer-scripts.hbs
new file mode 100644
index 0000000..ed7f49b
--- /dev/null
+++ b/supplemental-ui/partials/footer-scripts.hbs
@@ -0,0 +1,6 @@
+
+
+
+{{#if env.SITE_SEARCH_PROVIDER}}
+{{> search-scripts}}
+{{/if}}
diff --git a/supplemental-ui/partials/head-styles.hbs b/supplemental-ui/partials/head-styles.hbs
new file mode 100644
index 0000000..3e9d47d
--- /dev/null
+++ b/supplemental-ui/partials/head-styles.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/supplemental-ui/partials/header-content.hbs b/supplemental-ui/partials/header-content.hbs
new file mode 100644
index 0000000..eec2283
--- /dev/null
+++ b/supplemental-ui/partials/header-content.hbs
@@ -0,0 +1,41 @@
+
From 954ce517179ffcdea8e89eca3f5e401069781648 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 19 May 2026 11:09:49 +0000
Subject: [PATCH 2/6] fix: clarify header logo accessibility label
Agent-Logs-Url: https://github.com/PMSFIT/smartse-template/sessions/464d05ce-653d-4baf-a4b2-342b51f17b2f
Co-authored-by: pmai <91502+pmai@users.noreply.github.com>
---
supplemental-ui/partials/header-content.hbs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/supplemental-ui/partials/header-content.hbs b/supplemental-ui/partials/header-content.hbs
index eec2283..f7aff9e 100644
--- a/supplemental-ui/partials/header-content.hbs
+++ b/supplemental-ui/partials/header-content.hbs
@@ -1,7 +1,7 @@