diff --git a/NEWS.adoc b/NEWS.adoc index 8da073ce8e..a754d4c7eb 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -480,6 +480,12 @@ https://github.com/networkupstools/nut/milestone/13 Directory names are based on hashes of strings that identify the content, which are size-limited to some 8 characters. [issues #3108, #3390, #1711, #3526, PR #3109] + * Introduced `tools/asciidoc-github-links.pl` to simplify `docs/Makefile.am` + generation of `*.adoc-parsed` files with substituted GitHub links, and to + allow that logic to be executed separately from `make` and potentially be + re-used by other projects. Added support for lists of PRs/issues as used + in this document, and support for GitHub Security Advisories (GHSA). + [PR #3594, issue #1953] * Rearranged the GHA job to prepare Dist and Docs Tarballs. [PR #3460] * Updated `scripts/Windows/dllldd.sh` helper script to better detect DLL names not exposed with dynamic linking metadata (e.g. some Mozilla NSS diff --git a/docs/Makefile.am b/docs/Makefile.am index 25bb22bee0..446e658ce9 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -449,27 +449,20 @@ MAINTAINER_ASCIIDOCS_CHANGELOG_DEBUG = no fi ; \ exit 0 ; \ fi ; \ - echo " DOC-ASCIIDOC-GITHUB-LINKS Parsing GitHub link patterns $< => $@"; \ - if [ x"$(MAINTAINER_ASCIIDOCS_CHANGELOG_DEBUG)" != xno ] ; then \ - ls -lad $@ $< || true ; \ - stat $@ $< || true ; \ - fi ; \ - cat '$<' | { $(SED) \ - -e 's%\(link:https*://github.com/networkupstools/[a-zA-Z0-9./-]*/[1-9][0-9]*/*\[[^]]*\)\#\([1-9][0-9]*\)%\1\#\#\2%g' \ - -e 's%\[\([^]]*\)](\(https*://[^ ]*\))%link:\2[\1]%g' \ - -e 's%\(link:https*://github.com/[^ ]*\)\.\.\.\([^ ]*\)%\1..\2%g' \ - -e 's%\(issue\) *\#\([1-9][0-9]*\)\([^0-9]\|$$\)%link:https://github.com/networkupstools/nut/issues/\2[\1 \#\#\2]\3%g' \ - -e 's%\(PR\|pull request\) *\#\([1-9][0-9]*\)\([^0-9]\|$$\)%link:https://github.com/networkupstools/nut/pull/\2[\1 \#\#\2]\3%g' \ - -e 's%\([[ ,]\)\#\([1-9][0-9]*\)\([^0-9]\|$$\)%\1link:https://github.com/networkupstools/nut/issues/\2[\#\#\2]\3%g' \ - -e 's%\(issue\) networkupstools/\([^ ][^ ]*\)\#\([1-9][0-9]*\)\([^0-9]\|$$\)%link:https://github.com/networkupstools/\2/issues/\3[\1 \2\#\#\3]\4%g' \ - -e 's%\(PR\|pull request\) *networkupstools/\([^ ][^ ]*\)\#\([1-9][0-9]*\)\([^0-9]\|$$\)%link:https://github.com/networkupstools/\2/pull/\3[\1 \2\#\#\3]\4%g' \ - -e 's%\([[ ,]\)networkupstools/\([^ ][^ ]*\)\#\([1-9][0-9]*\)\([^0-9]\|$$\)%\1link:https://github.com/networkupstools/\2/issues/\3[\2\#\#\3]\4%g' \ - -e 's%\#\(\#[1-9][0-9]*\)%\1%g' \ - -e 's,\(https*://[^ \+]*\)[\]*[+],\1%2B,g' \ - ; } > "$@.tmp.$$$$" \ - && test -s "$@.tmp.$$$$" \ - && mv -f "$@.tmp.$$$$" '$@' \ - || { RES="$$?" ; rm -f "$@.tmp.$$$$" ; exit $$RES ; } + if perl -e 1; then \ + echo " DOC-ASCIIDOC-GITHUB-LINKS Parsing GitHub link patterns $< => $@"; \ + if [ x"$(MAINTAINER_ASCIIDOCS_CHANGELOG_DEBUG)" != xno ] ; then \ + ls -lad $@ $< || true ; \ + stat $@ $< || true ; \ + fi ; \ + $(top_srcdir)/tools/asciidoc-github-links.pl < '$<' > "$@.tmp.$$$$" \ + && test -s "$@.tmp.$$$$" \ + && mv -f "$@.tmp.$$$$" '$@' \ + || { RES="$$?" ; rm -f "$@.tmp.$$$$" ; exit $$RES ; } ; \ + else \ + echo " DOC-ASCIIDOC-GITHUB-LINKS SKIP: perl not available" >&2 ; \ + cat '$<' > '$@' ; \ + fi @if [ x'$@' = x'$(top_builddir)/ChangeLog.adoc-parsed' ] ; then \ touch -r '$@' '$(top_builddir)/docs/.ChangeLog.adoc-parsed.latest' || touch '$(top_builddir)/docs/.ChangeLog.adoc-parsed.latest' ; \ fi diff --git a/docs/nut.dict b/docs/nut.dict index 2fb4a9e1da..e8aeb4b415 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3818 utf-8 +personal_ws-1.1 en 3819 utf-8 AAC AAS ABI @@ -449,6 +449,7 @@ GES GETADDRINFO GETPID GHA +GHSA GID GIO GITREV @@ -3153,10 +3154,10 @@ renderer renderers renice repindex +replug repo reportId reposurgeon -replug repotec req resetter diff --git a/tools/Makefile.am b/tools/Makefile.am index 62406a12ee..c6ff505852 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -26,7 +26,8 @@ PYTHON_DEFAULT = @PYTHON_DEFAULT@ EXTRA_DIST = nut-usbinfo.pl nut-recorder.sh nut-ddl-dump.sh nut-dumpdiff.sh \ gitlog2changelog.py.in gitlog2version.sh semver-compare.sh \ - nut-snmpinfo.py.in driver-list-format.sh check-source-nonascii.pl + nut-snmpinfo.py.in driver-list-format.sh check-source-nonascii.pl \ + asciidoc-github-links.pl # These files are generated for nut-scanner builds (and cleaned as any others), # and can change as respective SNMP/USB subdriver sources are iterated by a @@ -164,6 +165,20 @@ dist-hook: echo "----------------------------------------------------------------------"; \ fi +# Using $? here for "sources newer than target", +# note that $< is not portable in non-suffix rules: +check-asciidoc-github-links: $(top_srcdir)/tools/asciidoc-github-links.pl + @echo "Checking for perl support of github links parser..." + @if perl -e 1; then \ + OUT="`echo 'Some changes. [#333, PRs #123, #345; issues #789, #99, advisories GHSA-1234-5678-90ab, #12345]' | perl '$?'`" || exit ; \ + test x"$$OUT" = x'Some changes. [link:https://github.com/networkupstools/nut/issues/333[#333], PRs link:https://github.com/networkupstools/nut/pull/123[#123], link:https://github.com/networkupstools/nut/pull/345[#345]; issues link:https://github.com/networkupstools/nut/issues/789[#789], link:https://github.com/networkupstools/nut/issues/99[#99], advisories link:https://github.com/networkupstools/nut/security/advisories/GHSA-1234-5678-90ab[GHSA-1234-5678-90ab], link:https://github.com/networkupstools/nut/issues/12345[#12345]]' || exit ; \ + echo "OK: $? returned expected output" ; \ + else \ + echo "SKIPPED: perl not available"; \ + fi + +check-local: check-asciidoc-github-links + MAINTAINERCLEANFILES = Makefile.in .dirstamp # Can be recreated by `make` or `configure`, diff --git a/tools/asciidoc-github-links.pl b/tools/asciidoc-github-links.pl new file mode 100755 index 0000000000..fdcda95b92 --- /dev/null +++ b/tools/asciidoc-github-links.pl @@ -0,0 +1,133 @@ +#!/usr/bin/env perl +# +# Convert certain text patterns into ASCIIDOC markup for GitHub links to +# issues, PRs, security advisories, etc. +# +# Copyright (C) 2023-2026 by Jim Klimov +# Based on earlier work with a stack of SED expressions in NUT::docs/Makefile.am + +use strict; + +# These may be eventually overridden by caller to re-use with other projects +# (need to add CLI or envvar inputs): +my $gh_orgname = "networkupstools"; +my $gh_prjname = "nut"; + +# NOTE: Schema in regexes would vary, but in injected URLs must be specific +my $gh_schema_re = qr/[Hh][Tt][Tt][Pp][Ss]?:\/\//; +my $gh_schema = "https://"; +my $gh_hostname_re = qr/github\.com/; +my $gh_hostname = "github.com"; + +# URI parts (under a project base URI) for singular item; note not all are +# consistently named singular/plural - for item vs. list). Also note that +# historically "issues" and "pulls" were handled by the same github-side +# handler so their URI parts could be interchangeable: +my $gh_uripart_pull = "pull"; +my $gh_uripart_issue = "issues"; +my $gh_uripart_secadv = "security/advisories"; + +my $issue_id_re = qr/[1-9][0-9]*/; +my $ghsa_id_re = qr/GHSA-[A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9]-[A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9]-[A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9]/; + +my $end_issue_id_re = qr/[^0-9]|$/; +my $end_ghsa_id_re = qr/[^A-Za-z0-9]|$/; + +# Values for easier substitution below: +my $gh_url_site = "${gh_schema}${gh_hostname}"; +my $gh_url_org = "${gh_url_site}/${gh_orgname}"; +my $gh_url_prj = "${gh_url_org}/${gh_prjname}"; + +local $/ = undef; +while (<>) { + # 1. link to a sibling project's issue/pull/advisory e.g. [networkupstools/nut#2048] + s/(link:${gh_schema_re}${gh_hostname_re}\/${gh_orgname}\/[a-zA-Z0-9.\/-]+\/[1-9][0-9]*\/*\[[^]]*)\#([1-9][0-9]*)/$1##$2/g; + + # 2. markdown links [text](url) -> link:url[text] + s/\[([^]]*)\]\((${gh_schema_re}[^ ]*)\)/{ "link:" . $2 . "[" . $1 . "]" }/ge; + + # 3. link ellipsis ... + s/(link:${gh_schema_re}${gh_hostname_re}\/[^ ]*)\.\.\.([^ ]*)/$1..$2/g; + + # 4. Lists of references: PRs/issues/advisories token followed by + # comma and/or space-separated identifiers, with state memory + s%\b(PRs|issues|advisories)\b(.*)%{ + my $plural = $1; + my $rest = $2; + my $out = ""; + + while ($rest ne "") { + my $uripart = + ($plural eq 'PRs') ? $gh_uripart_pull : + ($plural eq 'issues') ? $gh_uripart_issue : + $gh_uripart_secadv; + + $out .= $plural; + my $id_matcher = $plural eq 'advisories' ? qr/\#?(${ghsa_id_re})/ : qr/\#(${issue_id_re})/; + while ($rest =~ s/^(\s*(?:[;,]\s*)?)${id_matcher}//s) { + my $sep = $1; + my $id = $2; + + $out .= $sep; + if ($id =~ /^${ghsa_id_re}/) { + $out .= "link:${gh_url_prj}/${uripart}/" . $id . "[" . $id . "]"; + } + else { + my $num = $id; + $num =~ s/^\#+//; + $out .= "link:${gh_url_prj}/${uripart}/" . $num . "[#" . $num . "]"; + } + } + + if ($rest =~ s/^([;,]?\s*)(PRs|issues|advisories)(.*)//s) { + my $sep = $1; + $plural = $2; + $rest = $3; + + $out .= $sep; + } else { + last; + } + } + + $out . $rest; + }%ges; + + # 5. single [#GHSA-...] + s%(^|[^A-Za-z0-9])\[#?(${ghsa_id_re})\]%{ $1 . "[link:${gh_url_prj}/${gh_uripart_secadv}/" . $2 . "[" . $2 . "]]" }%ge; + + # 6. "advisory GHSA-..." or "advisory #GHSA-..." (multiple words / space separated, can be broken across lines) + s%\b(advisory)\s*\#?(${ghsa_id_re})(${end_ghsa_id_re})%{ "link:${gh_url_prj}/${gh_uripart_secadv}/" . $2 . "[" . $1 . " " . $2 . "]" . $3 }%ge; + + # 7. issue #123 + s%\b(issue)\s*\#(${issue_id_re})(${end_issue_id_re})%{ "link:${gh_url_prj}/${gh_uripart_issue}/" . $2 . "[" . $1 . " ##" . $2 . "]" . $3 }%ge; + + # 8. PR #123 or pull request #123 (multi-word "pull request" can be broken across lines) + s%\b(PR|pull\s+request)\s*\#(${issue_id_re})(${end_issue_id_re})%{ "link:${gh_url_prj}/${gh_uripart_pull}/" . $2 . "[" . $1 . " ##" . $2 . "]" . $3 }%ge; + + # 9. " #123" or ",#123" + s%([,\s])\#(${issue_id_re})(${end_issue_id_re})%{ $1 . "link:${gh_url_prj}/${gh_uripart_issue}/" . $2 . "[##" . $2 . "]" . $3 }%ge; + + # 10. "...[#123..." + s%(^|\D)\[\#(${issue_id_re})(${end_issue_id_re})%{ $1 . "[link:${gh_url_prj}/${gh_uripart_issue}/" . $2 . "[##" . $2 . "]" . $3 }%ge; + + # 11. issue networkupstools/foo#123 + s%\b(issue)\s+${gh_orgname}\/([^ \s]+)\#(${issue_id_re})(${end_issue_id_re})%{ "link:${gh_url_org}/" . $2 . "/" . $gh_uripart_issue . "/" . $3 . "[" . $1 . " " . $2 . "##" . $3 . "]" . $4 }%ge; + + # 12. PR networkupstools/foo#123 or pull request networkupstools/foo#123 (multi-word) + s%\b(PR|pull\s+request)\s+${gh_orgname}\/([^ \s]+)\#(${issue_id_re})(${end_issue_id_re})%{ "link:${gh_url_org}/" . $2 . "/" . $gh_uripart_pull . "/" . $3 . "[" . $1 . " " . $2 . "##" . $3 . "]" . $4 }%ge; + + # 13. [ ,]networkupstools/foo#123 + s%([,\s])${gh_orgname}\/([^ \s]+)\#(${issue_id_re})(${end_issue_id_re})%{ $1 . "link:${gh_url_org}/" . $2 . "/" . $gh_uripart_issue . "/" . $3 . "[" . $2 . "##" . $3 . "]" . $4 }%ge; + + # 14. ##123 -> #123 + s/\#(\#${issue_id_re})/$1/g; + + # 15. URL plus sign encoding + s/(${gh_schema_re}[^\s\+]*)([\]]*\+)/{ + "$1%2B"; + # printf stderr "[%2B]: Matched for '$1'\n"; + }/ge; + + print $_; +}