Skip to content

Commit 1f4f385

Browse files
committed
chore(dc-init): update workflows + actions
1 parent 60dd34d commit 1f4f385

5 files changed

Lines changed: 662 additions & 32 deletions

File tree

.github/workflows/humanise.yml

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# Template created by https://github.com/XAOSTECH/dev-control
2+
# See templates folder documentation for details.
3+
4+
# ============================================================================
5+
# Comment & Prose Humanisation Workflow Template (humanise.yml)
6+
# ============================================================================
7+
#
8+
# ORIGIN: https://github.com/XAOSTECH/dev-control
9+
# This workflow template is part of the dev-control toolkit.
10+
# See: https://github.com/XAOSTECH/dev-control/blob/main/workflows-templates/
11+
#
12+
# ============================================================================
13+
#
14+
# Rejoins editor/AI hard-wrapped comment and prose paragraphs onto single logical lines, removing the ~70-column wrap imposed on generated text.
15+
# A wrapped continuation is long, ends mid-sentence (no terminal punctuation) and continues in lower case; deliberate short lines, bullets, headings, tables, code blocks, SPDX headers and shebangs are left untouched.
16+
# The rule set is self-contained (embedded Perl) so it works even when only the workflow is copied into a target repository.
17+
#
18+
# TRIGGERS:
19+
# - Manual dispatch (workflow_dispatch)
20+
# - Monthly schedule
21+
#
22+
# ============================================================================
23+
24+
name: Humanise Comments and Prose
25+
26+
on:
27+
schedule:
28+
- cron: '0 3 15 * *'
29+
workflow_dispatch:
30+
inputs:
31+
extensions:
32+
description: 'Comma-separated file extensions to scan'
33+
required: false
34+
type: string
35+
default: 'sh,bash,zsh,md,markdown,txt,yml,yaml,py,rb,pl,toml,ts,tsx,js,jsx,go,rs,c,cc,cpp,h,hpp,java'
36+
min_wrap:
37+
description: 'Minimum line length treated as a hard wrap'
38+
required: false
39+
type: number
40+
default: 48
41+
dry_run:
42+
description: 'Report changes without opening a pull request'
43+
required: false
44+
type: boolean
45+
default: false
46+
47+
permissions:
48+
contents: write
49+
pull-requests: write
50+
51+
env:
52+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
53+
54+
# One humanise run per ref at a time so overlapping runs cannot race.
55+
concurrency:
56+
group: humanise-${{ github.ref }}
57+
cancel-in-progress: true
58+
59+
jobs:
60+
humanise:
61+
name: Humanise Comments and Prose
62+
runs-on: ubuntu-latest
63+
64+
steps:
65+
- name: Generate App Token
66+
id: app_token
67+
uses: actions/create-github-app-token@v2
68+
with:
69+
app-id: ${{ secrets['XB_AI'] }}
70+
private-key: ${{ secrets['XB_PK'] }}
71+
72+
- name: Checkout code
73+
uses: actions/checkout@v6
74+
with:
75+
fetch-depth: 0
76+
token: ${{ steps.app_token.outputs.token }}
77+
78+
- name: Humanise comments and prose
79+
id: humanise
80+
env:
81+
EXTENSIONS: ${{ inputs.extensions || 'sh,bash,zsh,md,markdown,txt,yml,yaml,py,rb,pl,toml,ts,tsx,js,jsx,go,rs,c,cc,cpp,h,hpp,java' }}
82+
HUMANISE_MIN_WRAP: ${{ inputs.min_wrap || '48' }}
83+
DRY_RUN: ${{ inputs.dry_run || 'false' }}
84+
run: |
85+
# Embed the reflow engine so the workflow is self-contained (no dependency on repo scripts).
86+
cat > /tmp/humanise.pl <<'HUMANISE_PL'
87+
#!/usr/bin/env perl
88+
# Reflow hard-wrapped paragraphs (comments and prose) onto single logical lines.
89+
# Editors/AI wrap comment and prose text at a fixed column (~70) mid-sentence;
90+
# we rejoin each wrapped paragraph. A wrapped continuation is long, ends
91+
# mid-sentence (no terminal punctuation) and continues in lower case; deliberate
92+
# short lines and all structure (bullets, headings, tables, code, SPDX, shebangs)
93+
# are emitted verbatim.
94+
use strict;
95+
use warnings;
96+
97+
my $MIN = $ENV{HUMANISE_MIN_WRAP} // 48;
98+
my $file = shift @ARGV or die "usage: humanise.pl FILE\n";
99+
100+
open my $in, '<', $file or die "open $file: $!\n";
101+
my @l = <$in>;
102+
close $in;
103+
my $final_nl = (@l && $l[-1] =~ /\n\z/) ? 1 : 0;
104+
chomp @l;
105+
106+
my $mode;
107+
if ($file =~ /\.(?:md|markdown|mdown|txt|text)\z/i) { $mode = 'md'; }
108+
elsif ($file =~ /\.(?:sh|bash|zsh|ya?ml|py|rb|pl|pm|toml|cfg|conf|ini|jl|mk)\z/i
109+
|| $file =~ m{(?:^|/)(?:Dockerfile|Makefile)[^/]*\z}) { $mode = 'hash'; }
110+
elsif ($file =~ /\.(?:ts|tsx|js|jsx|mjs|cjs|c|cc|cpp|cxx|h|hpp|hh|go|rs|java|kt|kts|swift|scala)\z/i) { $mode = 'slash'; }
111+
else { exit 0; }
112+
113+
my $mk = $mode eq 'hash' ? '#' : $mode eq 'slash' ? '//' : '';
114+
115+
sub is_url { return $_[0] =~ m{https?://|ftp://|file://}; }
116+
sub is_spdx { return $_[0] =~ /SPDX-|Copyright|\(c\)\s|\x{00A9}/i; }
117+
sub filled {
118+
my ($s) = @_;
119+
return 0 if length($s) < $MIN;
120+
return 0 if $s =~ /\\\z/ || $s =~ / \z/;
121+
return 0 if is_url($s);
122+
(my $t = $s) =~ s/\s+\z//;
123+
return 0 if $t =~ /[.!?:;]\z/;
124+
return 1;
125+
}
126+
sub cont_lc { return $_[0] =~ /^[a-z]/; }
127+
128+
sub comment_parts {
129+
my ($line, $mk) = @_;
130+
my $q = quotemeta $mk;
131+
return () unless $line =~ /^(\s*)$q\s?(\S.*)\z/;
132+
my ($ind, $txt) = ($1, $2);
133+
return () if $txt =~ /^!/;
134+
return () if $txt =~ /^[-=*#_~>|+]/;
135+
return () if $txt =~ /^\d+[.)]\s/;
136+
return () if $txt =~ /^(?:shellcheck|noqa|type:|pylint|eslint|prettier|TODO|FIXME|NOTE|HACK|XXX|@)/i;
137+
return () if is_spdx($txt) || is_url($line);
138+
return ($ind, $txt);
139+
}
140+
141+
sub reflow_comments {
142+
my ($lines, $mk) = @_;
143+
my @out;
144+
my $i = 0;
145+
while ($i <= $#$lines) {
146+
my $line = $lines->[$i];
147+
my ($ind, $txt) = comment_parts($line, $mk);
148+
if (!defined $ind) { push @out, $line; $i++; next; }
149+
my @para = ($txt);
150+
my @src = ($line);
151+
my $j = $i + 1;
152+
while ($j <= $#$lines) {
153+
my ($ind2, $txt2) = comment_parts($lines->[$j], $mk);
154+
last if !defined $ind2 || $ind2 ne $ind;
155+
push @para, $txt2;
156+
push @src, $lines->[$j];
157+
$j++;
158+
}
159+
my @merged = ({ txt => $para[0], src => $src[0], hit => 0 });
160+
for (my $k = 1; $k < @para; $k++) {
161+
if (filled($src[$k-1]) && cont_lc($para[$k])) {
162+
$merged[-1]{txt} .= ' ' . $para[$k];
163+
$merged[-1]{hit} = 1;
164+
} else {
165+
push @merged, { txt => $para[$k], src => $src[$k], hit => 0 };
166+
}
167+
}
168+
push @out, map { $_->{hit} ? "$ind$mk " . $_->{txt} : $_->{src} } @merged;
169+
$i = $j;
170+
}
171+
return @out;
172+
}
173+
174+
sub structural_md {
175+
my ($s) = @_;
176+
return ($s =~ /^\s*\z/
177+
|| $s =~ /^\s*\x60{3}/
178+
|| $s =~ /^\s{0,3}#{1,6}\s/
179+
|| $s =~ /^\s*[-*+]\s/
180+
|| $s =~ /^\s*\d+[.)]\s/
181+
|| $s =~ /^\s*>/
182+
|| $s =~ /^\s*\|/
183+
|| $s =~ /^\s*<[^>]/
184+
|| $s =~ /^\s{4,}\S/
185+
|| $s =~ /^\s*(?:[-=_*]\s*){3,}\z/);
186+
}
187+
188+
sub reflow_md {
189+
my ($lines) = @_;
190+
my @out;
191+
my $fence = 0;
192+
my $i = 0;
193+
if (@$lines && $lines->[0] =~ /^---\s*\z/) {
194+
push @out, $lines->[0]; $i = 1;
195+
while ($i <= $#$lines) { push @out, $lines->[$i]; my $done = ($lines->[$i] =~ /^---\s*\z/); $i++; last if $done; }
196+
}
197+
while ($i <= $#$lines) {
198+
my $line = $lines->[$i];
199+
if ($line =~ /^\s*\x60{3}/) { $fence = !$fence; push @out, $line; $i++; next; }
200+
if ($fence || structural_md($line)) { push @out, $line; $i++; next; }
201+
my @para = ($line);
202+
my $j = $i + 1;
203+
while ($j <= $#$lines && !structural_md($lines->[$j]) && $lines->[$j] !~ /^\s*\x60{3}/) {
204+
push @para, $lines->[$j];
205+
$j++;
206+
}
207+
my ($indent) = ($para[0] =~ /^(\s*)/);
208+
my @merged = ({ txt => ($para[0] =~ s/^\s+//r), src => $para[0], hit => 0 });
209+
for (my $k = 1; $k < @para; $k++) {
210+
(my $t = $para[$k]) =~ s/^\s+//;
211+
if (filled($para[$k-1]) && cont_lc($t)) {
212+
$merged[-1]{txt} .= ' ' . $t;
213+
$merged[-1]{hit} = 1;
214+
} else {
215+
push @merged, { txt => $t, src => $para[$k], hit => 0 };
216+
}
217+
}
218+
push @out, map { $_->{hit} ? $indent . $_->{txt} : $_->{src} } @merged;
219+
$i = $j;
220+
}
221+
return @out;
222+
}
223+
224+
my @out = $mode eq 'md' ? reflow_md(\@l) : reflow_comments(\@l, $mk);
225+
open my $o, '>', $file or die "write $file: $!\n";
226+
print $o join("\n", @out);
227+
print $o "\n" if $final_nl;
228+
close $o;
229+
HUMANISE_PL
230+
231+
# Candidate files: tracked, filtered by extension, excluding generated/vendor/self paths.
232+
IFS=',' read -ra EXTS <<< "$EXTENSIONS"
233+
FILES=()
234+
while IFS= read -r f; do
235+
case "$f" in
236+
.git/*|node_modules/*|*/node_modules/*|vendor/*|*/vendor/*) continue ;;
237+
test-repo/*|licence-templates/*|licences-templates/*|.github/static/*) continue ;;
238+
.github/workflows/humanise.yml|workflows-templates/humanise.yml) continue ;;
239+
LICENCE|LICENSE|*/LICENCE|*/LICENSE) continue ;;
240+
esac
241+
ext="${f##*.}"
242+
match=0
243+
for e in "${EXTS[@]}"; do
244+
[[ "$ext" == "$e" ]] && { match=1; break; }
245+
done
246+
[[ "$match" == 1 ]] && FILES+=("$f")
247+
done < <(git ls-files)
248+
249+
echo "Scanning ${#FILES[@]} files for hard-wrapped paragraphs..."
250+
if [[ ${#FILES[@]} -gt 0 ]]; then
251+
for f in "${FILES[@]}"; do
252+
perl /tmp/humanise.pl "$f" || echo "skip: $f"
253+
done
254+
fi
255+
256+
echo "## Humanise" >> "$GITHUB_STEP_SUMMARY"
257+
if git diff --quiet; then
258+
echo "needs_fix=false" >> "$GITHUB_OUTPUT"
259+
echo "No hard-wrapped comments or prose found." | tee -a "$GITHUB_STEP_SUMMARY"
260+
exit 0
261+
fi
262+
263+
COUNT=$(git diff --name-only | wc -l)
264+
echo "Reflowed hard-wrapped text in ${COUNT} file(s):" >> "$GITHUB_STEP_SUMMARY"
265+
git diff --name-only | sed 's/^/- /' >> "$GITHUB_STEP_SUMMARY"
266+
267+
if [[ "$DRY_RUN" == "true" ]]; then
268+
git checkout -- .
269+
echo "needs_fix=false" >> "$GITHUB_OUTPUT"
270+
echo "" >> "$GITHUB_STEP_SUMMARY"
271+
echo "_Dry run - no pull request opened._" >> "$GITHUB_STEP_SUMMARY"
272+
else
273+
echo "needs_fix=true" >> "$GITHUB_OUTPUT"
274+
fi
275+
276+
- name: Setup bot identity
277+
if: steps.humanise.outputs.needs_fix == 'true'
278+
uses: XAOSTECH/dev-control/.github/actions/identity@b067f72ca7f849b734a45634f23581a739d5146f # v2.0.0
279+
with:
280+
gpg-private-key: ${{ secrets['XB_GK'] }}
281+
gpg-passphrase: ${{ secrets['XB_GP'] }}
282+
user-token: ${{ secrets['XB_UT'] }}
283+
bot-name: ${{ vars.BOT_NAME || 'xaos-bot' }}
284+
285+
- name: Ensure automerge label exists
286+
if: steps.humanise.outputs.needs_fix == 'true'
287+
env:
288+
GH_TOKEN: ${{ steps.app_token.outputs.token }}
289+
run: |
290+
gh label create automerge --description "Automatically merge this PR" --color "3BEF67" 2>/dev/null || true
291+
292+
- name: Create Pull Request
293+
if: steps.humanise.outputs.needs_fix == 'true'
294+
env:
295+
GH_TOKEN: ${{ steps.app_token.outputs.token }}
296+
run: |
297+
git add -A
298+
if git diff --cached --quiet; then
299+
echo "No changes to commit"
300+
exit 0
301+
fi
302+
BRANCH_NAME="humanise/$(date +%Y%m%d-%H%M%S)"
303+
git checkout -b "$BRANCH_NAME"
304+
git commit -m "chore: humanise hard-wrapped comments and prose" -m "Rejoin editor/AI hard-wrapped comment and prose paragraphs onto single logical lines; structure, bullets, code, SPDX headers and deliberate short lines are preserved."
305+
git push origin "$BRANCH_NAME"
306+
gh pr create --title "chore: Humanise hard-wrapped comments and prose" --body "Automated reflow of editor/AI hard-wrapped comment and prose paragraphs onto single logical lines. Bullets, headings, tables, code blocks, SPDX headers, shebangs and deliberate short lines are preserved. Please review for any unintended joins. Generated by the Humanise workflow (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." --base "${{ github.ref_name }}" --head "$BRANCH_NAME" --label "automerge"
307+
echo "Pull request created."

0 commit comments

Comments
 (0)