Skip to content

Commit 88e8049

Browse files
committed
Enhance acknowledgements content and display it more prominently
Also add a Github workflow to generate them automatically with sophisticated content. Resolves - https://gitlab.eclipse.org/eclipse-wg/ide-wg/community/-/issues/77
1 parent 227d0fb commit 88e8049

File tree

9 files changed

+386
-29
lines changed

9 files changed

+386
-29
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
name: Generate Acknowledgements
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
eclipse-version:
6+
description: The version of the Eclipse-TLPs to be released. Something like '4.36'
7+
required: true
8+
type: string
9+
10+
permissions: {}
11+
12+
jobs:
13+
generate-acknowledgements:
14+
name: Generate Acknowledgements
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: write
18+
pull-requests: write
19+
steps:
20+
- name: Checkout Eclipse-Platform Releng Aggregator
21+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
22+
with:
23+
repository: eclipse-platform/eclipse.platform.releng.aggregator
24+
ref: master
25+
path: eclipse.platform.releng.aggregator
26+
- name: Checkout website
27+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
28+
with:
29+
path: website
30+
- name: Collect Eclipse TLP repositories
31+
id: collect-repos
32+
working-directory: eclipse.platform.releng.aggregator
33+
run: |
34+
repos=$(git config --file .gitmodules --get-regexp '\.url$' | awk '{print $2}' | tr '\n' ' ')
35+
echo "repos: ${repos}"
36+
echo "repos=${repos}" >> "$GITHUB_OUTPUT"
37+
- name: Collect contributors
38+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
39+
id: collect-contributors
40+
with:
41+
script: |
42+
const maxContributorsPerRow = 3
43+
let [major, minor] = '${{ inputs.eclipse-version }}'.split('.')
44+
const previousMinor = parseInt(minor) - 1
45+
const previousReleaseTag = 'R' + major + '_' + previousMinor
46+
let currentReleaseTag = 'master'
47+
if (await isTagAvailable('R' + major + '_' + minor)) {
48+
currentReleaseTag = 'R' + major + '_' + minor
49+
} else if (await isTagAvailable('S' + major + '_' + minor + '_0_RC2')) {
50+
currentReleaseTag = 'S' + major + '_' + minor + '_0_RC2'
51+
}
52+
53+
// ----------------------------------------------
54+
// Collect all repositories
55+
// ----------------------------------------------
56+
const submoduleURLs = '${{ steps.collect-repos.outputs.repos }}'.trim()
57+
console.log("Repo list is: " + submoduleURLs)
58+
const ghBaseURL = 'https://github.com/'
59+
const gitSuffix = '.git'
60+
const allRepos = submoduleURLs.split(' ').map(url => {
61+
if (!url.startsWith(ghBaseURL) || !url.endsWith(gitSuffix)) {
62+
core.error('Unsupported repository URL format: ' + url)
63+
throw new Error('Unsupported repository URL format: ' + url)
64+
}
65+
const repo = url.substring(ghBaseURL.length, url.length - gitSuffix.length)
66+
if (repo.split('/').length != 2) {
67+
throw new Error('Unsupported repository URL format: ' + url)
68+
}
69+
return repo
70+
})
71+
allRepos.unshift('eclipse-platform/eclipse.platform.releng.aggregator')
72+
console.log('All repositories: ' + allRepos)
73+
74+
// ----------------------------------------------
75+
// Collect the contributors for each organization
76+
// ----------------------------------------------
77+
console.log("Query all commits betweens tag '" + previousReleaseTag + "' and '" + currentReleaseTag + "'")
78+
const orgaContributors = new Map()
79+
const contributorNames = new Map()
80+
const profileReplacements = new Set()
81+
const skippedBotAccounts = new Set()
82+
for (const repo of allRepos) {
83+
let [organization, repository] = repo.split('/')
84+
let contributors = computeIfAbsent(orgaContributors, organization, () => new Set())
85+
console.log("Query for organization '" + organization + "' repository '" + repository + "'" )
86+
// Determine the date of the previous release commit
87+
const previousReleaseTagSHA = (await github.rest.git.getRef({
88+
owner: organization, repo: repository,
89+
ref: 'tags/' + previousReleaseTag,
90+
})).data.object.sha
91+
const previousReleaseCommitSHA = (await github.rest.git.getTag({
92+
owner: organization, repo: repository,
93+
tag_sha: previousReleaseTagSHA,
94+
})).data.object.sha
95+
const previousReleaseCommitDate = Date.parse((await github.rest.git.getCommit({
96+
owner: organization, repo: repository,
97+
commit_sha: previousReleaseCommitSHA,
98+
})).data.committer.date)
99+
100+
// See https://octokit.github.io/rest.js/v21/#repos-compare-commits-with-basehead
101+
// About pagination, see https://github.com/octokit/octokit.js#pagination
102+
let responseIterator = github.paginate.iterator(github.rest.repos.compareCommitsWithBasehead, {
103+
owner: organization, repo: repository,
104+
basehead: previousReleaseTag + '...' + currentReleaseTag,
105+
per_page: 200,
106+
})
107+
let commitCount = 0
108+
for await (const response of responseIterator) { // iterate through each response
109+
for (const commitData of response.data.commits) {
110+
// console.log(JSON.stringify(commitData))
111+
if (Date.parse(commitData.commit.committer.date) < previousReleaseCommitDate){
112+
console.log("Skip commit committed before previous release (probably merged from older branch): " + commitData.sha)
113+
continue;
114+
}
115+
let authorName = commitData.commit.author.name
116+
if (commitData.author) {
117+
let profile = commitData.author.login
118+
if (isBot(commitData.commit.author)) { // Exclude contributors from bot-accounts
119+
skippedBotAccounts.add(profile)
120+
continue;
121+
}
122+
const committerProfile = commitData.committer?.login
123+
if (commitData.commit.author.name == commitData.commit.committer.name
124+
&& committerProfile && profile != committerProfile) {
125+
// Sometimes contributors use different profiles. Let the committer profile take precedence
126+
profileReplacements.add("@" + profile + " -> @" + committerProfile)
127+
profile = committerProfile
128+
}
129+
contributors.add(profile)
130+
computeIfAbsent(contributorNames, profile, () => new Set()).add(authorName)
131+
} else { // author is null for directly pushed commits, which happens e.g. for I-build submodule updates
132+
console.log("Skip commit of " + authorName)
133+
}
134+
commitCount++
135+
}
136+
}
137+
console.log('Processed commits: ' + commitCount)
138+
}
139+
140+
// ------------------------------------------------------
141+
// Select name if multiple have been found for one contributor
142+
// ------------------------------------------------------
143+
const selectedContributorNames = new Map()
144+
const nameInconsistencies = []
145+
for (const [profile, names] of contributorNames) {
146+
// Select longest name, assuming that's correct
147+
let selectedName = [...names].reduce((n1, n2) => n1.length > n2.length ? n1 : n2)
148+
if (names.size > 1) {
149+
console.log("Multiple names encountered for " + profile + ": " + Array.from(names).join(', '))
150+
nameInconsistencies.push("@" + profile + ": " + Array.from(names).map(n => n==selectedName ? ("**`" + n + "`**") : ("`" + n + "`")).join(', '))
151+
}
152+
selectedContributorNames.set(profile, selectedName)
153+
}
154+
155+
// ------------------------------------------------------
156+
// Insert the list of contributors into the template file
157+
// ------------------------------------------------------
158+
const fs = require('fs')
159+
const acknowledgementsFile = 'website/news/${{ inputs.eclipse-version }}/acknowledgements.md'
160+
let lines = fs.readFileSync(acknowledgementsFile, {encoding: 'utf8'}).split(/\r?\n/)
161+
let elementsInLine = 0
162+
for (const [organization, contributors] of orgaContributors) {
163+
console.log('Insert contributors of ' + organization)
164+
const startMarker = lines.indexOf('<!-- START: ' + organization + ' contributors -->')
165+
const endMarker = lines.indexOf('<!-- END: ' + organization + ' contributors -->')
166+
if (startMarker < 0 || endMarker < 0) {
167+
throw new Error('Start or end marker to found for organization: ' + organization)
168+
}
169+
const contributorEntries = Array.from(contributors, profile => {
170+
const name = selectedContributorNames.get(profile)
171+
if (!name) {
172+
throw new Error('No selected name for profile: ' + profile)
173+
}
174+
return [name, profile]
175+
})
176+
// Sort by name in ascending order
177+
contributorEntries.sort((e1, e2) => e1[0].localeCompare(e2[0]))
178+
179+
const contributorLines = ['|'.repeat(maxContributorsPerRow) + '|', '|---'.repeat(maxContributorsPerRow) + '|']
180+
let line = ''
181+
let elements = 0
182+
for (const [name, profileId] of contributorEntries) {
183+
line += ('| [' + name + '](' + ghBaseURL + profileId + ') ')
184+
if (++elements >= maxContributorsPerRow) {
185+
contributorLines.push(line + '|')
186+
line = ''
187+
elements = 0
188+
}
189+
}
190+
if (line.length !== 0) {
191+
contributorLines.push(line + ' |')
192+
}
193+
lines.splice(startMarker + 1, endMarker - (startMarker + 1), ...contributorLines)
194+
}
195+
// Update last-revised date
196+
const lastRevisedLineIndex = lines.findIndex(l => l.startsWith('Last revised: '))
197+
lines[lastRevisedLineIndex] = 'Last revised: ' + new Date().toLocaleDateString("en-US", {
198+
year: "numeric",
199+
month: "long",
200+
day: "numeric",
201+
})
202+
fs.writeFileSync(acknowledgementsFile, lines.join('\n'), {encoding: 'utf8'})
203+
204+
// Set adjustments as outputs in order to append them to the PR message
205+
core.setOutput('profile-replacements', Array.from(profileReplacements).map(r => " - " + r).join("\n"));
206+
core.setOutput('skipped-bots', Array.from(skippedBotAccounts).map(b => " - @" + b).join("\n"));
207+
core.setOutput('name-inconsistencies', nameInconsistencies.map(l => " - " + l).join("\n"));
208+
209+
function isTagAvailable(tagName) {
210+
return github.rest.git.getRef({
211+
owner: 'eclipse-platform', repo: 'eclipse.platform.releng.aggregator',
212+
ref: 'tags/' + tagName,
213+
}).then(value => {
214+
console.log("Tag found: " + tagName)
215+
return value.data.object.type == 'tag';
216+
}, error => {
217+
console.log("Tag not found: " + tagName)
218+
return false;
219+
});
220+
}
221+
222+
function isBot(author) {
223+
return author.email.endsWith("[email protected]") || author.email.endsWith("[bot]@users.noreply.github.com") || author.name == 'eclipse-releng-bot'
224+
}
225+
226+
function computeIfAbsent(map, key, valueSupplier) {
227+
let value = map.get(key)
228+
if (!value) {
229+
value = valueSupplier()
230+
map.set(key, value)
231+
}
232+
return value
233+
}
234+
235+
- name: Create Acknowledgements Update PR
236+
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
237+
with:
238+
path : website
239+
author: Eclipse Releng Bot <[email protected]>
240+
commit-message: Update Acknowledgements for ${{ inputs.eclipse-version }}
241+
branch: acknowledgements_${{ inputs.eclipse-version }}
242+
title: Update Acknowledgements for ${{ inputs.eclipse-version }}
243+
body: |
244+
Update the list of contributors in the Acknowledgements for `${{ inputs.eclipse-version }}`.
245+
246+
Adjustments to the lists of contributors:
247+
- Replaced profiles:
248+
${{ steps.collect-contributors.outputs.profile-replacements && steps.collect-contributors.outputs.profile-replacements || 'None' }}
249+
- Profiles with inconsistent git author names:
250+
_To avoid this in the future, please ensure you use the same author names across all your local git repositories (e.g. by setting `git config --global user.name "Your Name"`) and across devices!
251+
If the selected name, simply the longest one (and marked in bold), is incorrect, please let us know._
252+
${{ steps.collect-contributors.outputs.name-inconsistencies && steps.collect-contributors.outputs.name-inconsistencies || 'None' }}
253+
- Excluded bot-accounts:
254+
${{ steps.collect-contributors.outputs.skipped-bots && steps.collect-contributors.outputs.skipped-bots || 'None' }}
255+
256+
Please verify these adjustments for correctness and grant those who are affected sufficient time to refine the adjustments.
257+
delete-branch: true

markdown/index.html

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
padding: 0;
4141
margin: 0;
4242
margin-left: 0.5em;
43-
4443
}
4544

4645
.tl1 {
@@ -130,23 +129,32 @@
130129
transition: .2s;
131130
}
132131

132+
p .avatar,
133133
summary .avatar {
134134
width: 2em;
135135
}
136136

137+
td .avatar,
137138
li .avatar {
138139
width: 3.5em;
139140
}
140141

142+
p .avatar:hover,
141143
summary .avatar:hover {
142144
transform: scale(3);
143145
}
144146

147+
td .avatar:hover,
145148
li .avatar:hover,
146149
.avatar-hover {
147150
transform: scale(2);
148151
}
149152

153+
/* intended for table, tr, th, td */
154+
.contributor-list {
155+
border: none;
156+
}
157+
150158
/*]]>*/
151159
</style>
152160
</head>
@@ -296,23 +304,14 @@ <h2>Table of Contents</h2>
296304
if (ul?.localName == 'ul') {
297305
const details = ul.parentElement;
298306
if (details?.localName == 'details') {
299-
const avatar = toElements(`<span>${generateAvatar(logicalHref)}&nbsp;</span>`)[0];
300-
li.insertBefore(avatar, a);
307+
injectAvatars(a, logicalHref)
301308

302309
const summary = details.querySelector('summary');
303310
if (summary.children.length == 0) {
304311
summary.innerHTML += '&nbsp;';
305312
}
306313
summary.innerHTML += generateAvatar(logicalHref);
307314

308-
a.onmouseenter = () => {
309-
avatar.querySelector('img').classList.add('avatar-hover')
310-
};
311-
a.onmouseleave = () => {
312-
avatar.querySelector('img').classList.remove('avatar-hover')
313-
};
314-
315-
316315
// We only need to process the overall contributor section once.
317316
if (ul.style.listStyleType != 'none') {
318317
ul.style.listStyleType = 'none';
@@ -331,6 +330,39 @@ <h2>Table of Contents</h2>
331330
}
332331
}
333332
}
333+
334+
function generateContributorAvatars(a, logicalHref) {
335+
injectAvatars(a, logicalHref)
336+
const td = a.parentElement;
337+
// We only need to process the overall contributor table once.
338+
if (td.localName == 'td' && !td.classList.contains('contributor-list')) {
339+
const tr = td.parentElement;
340+
if (tr?.localName == 'tr') {
341+
const tbody = tr.parentElement;
342+
if (tbody?.localName == 'tbody') {
343+
const table = tbody.parentElement;
344+
if (table?.localName == 'table') {
345+
table.classList.add('contributor-list')
346+
const tableElements = table.querySelectorAll('th, tr, td');
347+
for (const e of tableElements) {
348+
e.classList.add('contributor-list')
349+
}
350+
}
351+
}
352+
}
353+
}
354+
}
355+
356+
function injectAvatars(a, logicalHref) {
357+
const avatar = toElements(`<span>${generateAvatar(logicalHref)}&nbsp;</span>`)[0];
358+
a.parentElement.insertBefore(avatar, a);
359+
a.onmouseenter = () => {
360+
avatar.querySelector('img').classList.add('avatar-hover')
361+
};
362+
a.onmouseleave = () => {
363+
avatar.querySelector('img').classList.remove('avatar-hover')
364+
};
365+
}
334366

335367
function generateMarkdown(logicalBaseURL, response) {
336368
if (response instanceof Array) {
@@ -376,6 +408,7 @@ <h2>Table of Contents</h2>
376408
}
377409

378410
const as = targetElement.querySelectorAll("a[href]");
411+
const isAcknowledgements = logicalBaseURL.pathname.endsWith("acknowledgements.md")
379412
for (const a of as) {
380413
const href = a.getAttribute('href');
381414
if (href == null) {
@@ -390,7 +423,11 @@ <h2>Table of Contents</h2>
390423
const logicalHref = new URL(href, logicalBaseURL);
391424
if (!logicalHref.pathname.endsWith('.md')) {
392425
if (/^https:\/\/github.com\/[^\/]+$/.exec(logicalHref.toString())) {
393-
generateContributorSection(a, logicalHref);
426+
if (isAcknowledgements) {
427+
generateContributorAvatars(a, logicalHref);
428+
} else {
429+
generateContributorSection(a, logicalHref);
430+
}
394431
} else {
395432
const siteURL = toSiteURL(logicalHref);
396433
if (siteURL != null) {

0 commit comments

Comments
 (0)