From 17660f5ac678cecabea7b6b1970aefcfdf755093 Mon Sep 17 00:00:00 2001 From: Hannes Wellmann Date: Tue, 20 May 2025 19:07:38 +0200 Subject: [PATCH] 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 --- .../workflows/generateAcknowledgements.yml | 257 ++++++++++++++++++ markdown/index.html | 61 ++++- news/4.x-template/acknowledgements.md | 43 +++ news/4.x-template/index.md | 4 +- news/4.x-template/jdt.md | 2 + news/4.x-template/pde.md | 2 + news/4.x-template/platform.md | 2 + news/4.x-template/platform_isv.md | 2 + project.js | 42 +-- 9 files changed, 386 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/generateAcknowledgements.yml create mode 100644 news/4.x-template/acknowledgements.md diff --git a/.github/workflows/generateAcknowledgements.yml b/.github/workflows/generateAcknowledgements.yml new file mode 100644 index 000000000..439f06c16 --- /dev/null +++ b/.github/workflows/generateAcknowledgements.yml @@ -0,0 +1,257 @@ +name: Generate Acknowledgements +on: + workflow_dispatch: + inputs: + eclipse-version: + description: The version of the Eclipse-TLPs to be released. Something like '4.36' + required: true + type: string + +permissions: {} + +jobs: + generate-acknowledgements: + name: Generate Acknowledgements + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout Eclipse-Platform Releng Aggregator + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: eclipse-platform/eclipse.platform.releng.aggregator + ref: master + path: eclipse.platform.releng.aggregator + - name: Checkout website + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: website + - name: Collect Eclipse TLP repositories + id: collect-repos + working-directory: eclipse.platform.releng.aggregator + run: | + repos=$(git config --file .gitmodules --get-regexp '\.url$' | awk '{print $2}' | tr '\n' ' ') + echo "repos: ${repos}" + echo "repos=${repos}" >> "$GITHUB_OUTPUT" + - name: Collect contributors + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: collect-contributors + with: + script: | + const maxContributorsPerRow = 3 + let [major, minor] = '${{ inputs.eclipse-version }}'.split('.') + const previousMinor = parseInt(minor) - 1 + const previousReleaseTag = 'R' + major + '_' + previousMinor + let currentReleaseTag = 'master' + if (await isTagAvailable('R' + major + '_' + minor)) { + currentReleaseTag = 'R' + major + '_' + minor + } else if (await isTagAvailable('S' + major + '_' + minor + '_0_RC2')) { + currentReleaseTag = 'S' + major + '_' + minor + '_0_RC2' + } + + // ---------------------------------------------- + // Collect all repositories + // ---------------------------------------------- + const submoduleURLs = '${{ steps.collect-repos.outputs.repos }}'.trim() + console.log("Repo list is: " + submoduleURLs) + const ghBaseURL = 'https://github.com/' + const gitSuffix = '.git' + const allRepos = submoduleURLs.split(' ').map(url => { + if (!url.startsWith(ghBaseURL) || !url.endsWith(gitSuffix)) { + core.error('Unsupported repository URL format: ' + url) + throw new Error('Unsupported repository URL format: ' + url) + } + const repo = url.substring(ghBaseURL.length, url.length - gitSuffix.length) + if (repo.split('/').length != 2) { + throw new Error('Unsupported repository URL format: ' + url) + } + return repo + }) + allRepos.unshift('eclipse-platform/eclipse.platform.releng.aggregator') + console.log('All repositories: ' + allRepos) + + // ---------------------------------------------- + // Collect the contributors for each organization + // ---------------------------------------------- + console.log("Query all commits betweens tag '" + previousReleaseTag + "' and '" + currentReleaseTag + "'") + const orgaContributors = new Map() + const contributorNames = new Map() + const profileReplacements = new Set() + const skippedBotAccounts = new Set() + for (const repo of allRepos) { + let [organization, repository] = repo.split('/') + let contributors = computeIfAbsent(orgaContributors, organization, () => new Set()) + console.log("Query for organization '" + organization + "' repository '" + repository + "'" ) + // Determine the date of the previous release commit + const previousReleaseTagSHA = (await github.rest.git.getRef({ + owner: organization, repo: repository, + ref: 'tags/' + previousReleaseTag, + })).data.object.sha + const previousReleaseCommitSHA = (await github.rest.git.getTag({ + owner: organization, repo: repository, + tag_sha: previousReleaseTagSHA, + })).data.object.sha + const previousReleaseCommitDate = Date.parse((await github.rest.git.getCommit({ + owner: organization, repo: repository, + commit_sha: previousReleaseCommitSHA, + })).data.committer.date) + + // See https://octokit.github.io/rest.js/v21/#repos-compare-commits-with-basehead + // About pagination, see https://github.com/octokit/octokit.js#pagination + let responseIterator = github.paginate.iterator(github.rest.repos.compareCommitsWithBasehead, { + owner: organization, repo: repository, + basehead: previousReleaseTag + '...' + currentReleaseTag, + per_page: 200, + }) + let commitCount = 0 + for await (const response of responseIterator) { // iterate through each response + for (const commitData of response.data.commits) { + // console.log(JSON.stringify(commitData)) + if (Date.parse(commitData.commit.committer.date) < previousReleaseCommitDate){ + console.log("Skip commit committed before previous release (probably merged from older branch): " + commitData.sha) + continue; + } + let authorName = commitData.commit.author.name + if (commitData.author) { + let profile = commitData.author.login + if (isBot(commitData.commit.author)) { // Exclude contributors from bot-accounts + skippedBotAccounts.add(profile) + continue; + } + const committerProfile = commitData.committer?.login + if (commitData.commit.author.name == commitData.commit.committer.name + && committerProfile && profile != committerProfile) { + // Sometimes contributors use different profiles. Let the committer profile take precedence + profileReplacements.add("@" + profile + " -> @" + committerProfile) + profile = committerProfile + } + contributors.add(profile) + computeIfAbsent(contributorNames, profile, () => new Set()).add(authorName) + } else { // author is null for directly pushed commits, which happens e.g. for I-build submodule updates + console.log("Skip commit of " + authorName) + } + commitCount++ + } + } + console.log('Processed commits: ' + commitCount) + } + + // ------------------------------------------------------ + // Select name if multiple have been found for one contributor + // ------------------------------------------------------ + const selectedContributorNames = new Map() + const nameInconsistencies = [] + for (const [profile, names] of contributorNames) { + // Select longest name, assuming that's correct + let selectedName = [...names].reduce((n1, n2) => n1.length > n2.length ? n1 : n2) + if (names.size > 1) { + console.log("Multiple names encountered for " + profile + ": " + Array.from(names).join(', ')) + nameInconsistencies.push("@" + profile + ": " + Array.from(names).map(n => n==selectedName ? ("**`" + n + "`**") : ("`" + n + "`")).join(', ')) + } + selectedContributorNames.set(profile, selectedName) + } + + // ------------------------------------------------------ + // Insert the list of contributors into the template file + // ------------------------------------------------------ + const fs = require('fs') + const acknowledgementsFile = 'website/news/${{ inputs.eclipse-version }}/acknowledgements.md' + let lines = fs.readFileSync(acknowledgementsFile, {encoding: 'utf8'}).split(/\r?\n/) + let elementsInLine = 0 + for (const [organization, contributors] of orgaContributors) { + console.log('Insert contributors of ' + organization) + const startMarker = lines.indexOf('') + const endMarker = lines.indexOf('') + if (startMarker < 0 || endMarker < 0) { + throw new Error('Start or end marker to found for organization: ' + organization) + } + const contributorEntries = Array.from(contributors, profile => { + const name = selectedContributorNames.get(profile) + if (!name) { + throw new Error('No selected name for profile: ' + profile) + } + return [name, profile] + }) + // Sort by name in ascending order + contributorEntries.sort((e1, e2) => e1[0].localeCompare(e2[0])) + + const contributorLines = ['|'.repeat(maxContributorsPerRow) + '|', '|---'.repeat(maxContributorsPerRow) + '|'] + let line = '' + let elements = 0 + for (const [name, profileId] of contributorEntries) { + line += ('| [' + name + '](' + ghBaseURL + profileId + ') ') + if (++elements >= maxContributorsPerRow) { + contributorLines.push(line + '|') + line = '' + elements = 0 + } + } + if (line.length !== 0) { + contributorLines.push(line + ' |') + } + lines.splice(startMarker + 1, endMarker - (startMarker + 1), ...contributorLines) + } + // Update last-revised date + const lastRevisedLineIndex = lines.findIndex(l => l.startsWith('Last revised: ')) + lines[lastRevisedLineIndex] = 'Last revised: ' + new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }) + fs.writeFileSync(acknowledgementsFile, lines.join('\n'), {encoding: 'utf8'}) + + // Set adjustments as outputs in order to append them to the PR message + core.setOutput('profile-replacements', Array.from(profileReplacements).map(r => " - " + r).join("\n")); + core.setOutput('skipped-bots', Array.from(skippedBotAccounts).map(b => " - @" + b).join("\n")); + core.setOutput('name-inconsistencies', nameInconsistencies.map(l => " - " + l).join("\n")); + + function isTagAvailable(tagName) { + return github.rest.git.getRef({ + owner: 'eclipse-platform', repo: 'eclipse.platform.releng.aggregator', + ref: 'tags/' + tagName, + }).then(value => { + console.log("Tag found: " + tagName) + return value.data.object.type == 'tag'; + }, error => { + console.log("Tag not found: " + tagName) + return false; + }); + } + + function isBot(author) { + return author.email.endsWith("-bot@eclipse.org") || author.email.endsWith("[bot]@users.noreply.github.com") || author.name == 'eclipse-releng-bot' + } + + function computeIfAbsent(map, key, valueSupplier) { + let value = map.get(key) + if (!value) { + value = valueSupplier() + map.set(key, value) + } + return value + } + + - name: Create Acknowledgements Update PR + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + with: + path : website + author: Eclipse Releng Bot + commit-message: Update Acknowledgements for ${{ inputs.eclipse-version }} + branch: acknowledgements_${{ inputs.eclipse-version }} + title: Update Acknowledgements for ${{ inputs.eclipse-version }} + body: | + Update the list of contributors in the Acknowledgements for `${{ inputs.eclipse-version }}`. + + Adjustments to the lists of contributors: + - Replaced profiles: + ${{ steps.collect-contributors.outputs.profile-replacements && steps.collect-contributors.outputs.profile-replacements || 'None' }} + - Profiles with inconsistent git author names: + _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! + If the selected name, simply the longest one (and marked in bold), is incorrect, please let us know._ + ${{ steps.collect-contributors.outputs.name-inconsistencies && steps.collect-contributors.outputs.name-inconsistencies || 'None' }} + - Excluded bot-accounts: + ${{ steps.collect-contributors.outputs.skipped-bots && steps.collect-contributors.outputs.skipped-bots || 'None' }} + + Please verify these adjustments for correctness and grant those who are affected sufficient time to refine the adjustments. + delete-branch: true diff --git a/markdown/index.html b/markdown/index.html index 7d51320a4..95f94e1d9 100644 --- a/markdown/index.html +++ b/markdown/index.html @@ -40,7 +40,6 @@ padding: 0; margin: 0; margin-left: 0.5em; - } .tl1 { @@ -130,23 +129,32 @@ transition: .2s; } + p .avatar, summary .avatar { width: 2em; } + td .avatar, li .avatar { width: 3.5em; } + p .avatar:hover, summary .avatar:hover { transform: scale(3); } + td .avatar:hover, li .avatar:hover, .avatar-hover { transform: scale(2); } + /* intended for table, tr, th, td */ + .contributor-list { + border: none; + } + /*]]>*/ @@ -296,8 +304,7 @@

Table of Contents

if (ul?.localName == 'ul') { const details = ul.parentElement; if (details?.localName == 'details') { - const avatar = toElements(`${generateAvatar(logicalHref)} `)[0]; - li.insertBefore(avatar, a); + injectAvatars(a, logicalHref) const summary = details.querySelector('summary'); if (summary.children.length == 0) { @@ -305,14 +312,6 @@

Table of Contents

} summary.innerHTML += generateAvatar(logicalHref); - a.onmouseenter = () => { - avatar.querySelector('img').classList.add('avatar-hover') - }; - a.onmouseleave = () => { - avatar.querySelector('img').classList.remove('avatar-hover') - }; - - // We only need to process the overall contributor section once. if (ul.style.listStyleType != 'none') { ul.style.listStyleType = 'none'; @@ -331,6 +330,39 @@

Table of Contents

} } } + + function generateContributorAvatars(a, logicalHref) { + injectAvatars(a, logicalHref) + const td = a.parentElement; + // We only need to process the overall contributor table once. + if (td.localName == 'td' && !td.classList.contains('contributor-list')) { + const tr = td.parentElement; + if (tr?.localName == 'tr') { + const tbody = tr.parentElement; + if (tbody?.localName == 'tbody') { + const table = tbody.parentElement; + if (table?.localName == 'table') { + table.classList.add('contributor-list') + const tableElements = table.querySelectorAll('th, tr, td'); + for (const e of tableElements) { + e.classList.add('contributor-list') + } + } + } + } + } + } + + function injectAvatars(a, logicalHref) { + const avatar = toElements(`${generateAvatar(logicalHref)} `)[0]; + a.parentElement.insertBefore(avatar, a); + a.onmouseenter = () => { + avatar.querySelector('img').classList.add('avatar-hover') + }; + a.onmouseleave = () => { + avatar.querySelector('img').classList.remove('avatar-hover') + }; + } function generateMarkdown(logicalBaseURL, response) { if (response instanceof Array) { @@ -376,6 +408,7 @@

Table of Contents

} const as = targetElement.querySelectorAll("a[href]"); + const isAcknowledgements = logicalBaseURL.pathname.endsWith("acknowledgements.md") for (const a of as) { const href = a.getAttribute('href'); if (href == null) { @@ -390,7 +423,11 @@

Table of Contents

const logicalHref = new URL(href, logicalBaseURL); if (!logicalHref.pathname.endsWith('.md')) { if (/^https:\/\/github.com\/[^\/]+$/.exec(logicalHref.toString())) { - generateContributorSection(a, logicalHref); + if (isAcknowledgements) { + generateContributorAvatars(a, logicalHref); + } else { + generateContributorSection(a, logicalHref); + } } else { const siteURL = toSiteURL(logicalHref); if (siteURL != null) { diff --git a/news/4.x-template/acknowledgements.md b/news/4.x-template/acknowledgements.md new file mode 100644 index 000000000..da3aa206c --- /dev/null +++ b/news/4.x-template/acknowledgements.md @@ -0,0 +1,43 @@ +## Eclipse YYYY-MM Acknowledgements + +Last revised: + +We would also like to thank the users and adopters who support our efforts through a range of activities, including early testing, being a Friend of Eclipse, contracting special work, or outright employment. + +A special thanks goes to [Holger Voormann](https://github.com/howlger) for his Eclipse IDE promotion videos. + +Another special thanks to other Eclipse projects we build upon: EMF and ECF, who also provide timely updates so we can release on time. +We also thank the other Eclipse projects that make up part of the infrastructure we depend on: Tycho, Orbit, EGit, EMF, and ECF for providing fixes and steady improvements. + +## Eclipse Platform + +The Platform team would like to thank everyone who has helped us improve quality by testing and reporting bugs and enhancement requests. +Special thanks to all code contributors (alphabetically): + + + +## Java Development Tools + +The JDT team thanks everyone who filed good enhancement requests, helped improve quality by testing and filing bug reports, and provided answers on JDT forums/newsgroups. +Special thanks to all code contributors (alphabetically): + + + +## Plug-in Development Environment + +The Plug-in Development Environment team thanks numerous contributors who continue to improve the component every release. +Special thanks to all code contributors (alphabetically): + + + +## Equinox + +The Equinox team thanks all contributors who helped improve the project by filing bug reports and enhancement requests. +Special thanks to all code contributors (alphabetically): + + + +## Eclipse Foundation + +The entire Eclipse Project team would like to thank the [Eclipse Foundation staff](https://www.eclipse.org/org/foundation/staff/) for their tireless efforts and especially +[Frederic Gurr](https://github.com/fredg02) and [Sébastien Heurtematte](https://github.com/heurtematte) for not only keeping all that infrastructure going but also constantly improving it. diff --git a/news/4.x-template/index.md b/news/4.x-template/index.md index 088914204..a6f0ba83e 100644 --- a/news/4.x-template/index.md +++ b/news/4.x-template/index.md @@ -10,4 +10,6 @@ Here are some of the more noteworthy items available in this release. - [New features in the Platform and Equinox](platform.md) - [New features for Java developers](jdt.md) - [New APIs in the Platform and Equinox](platform_isv.md) -- [New features for plug-in developers](pde.md) \ No newline at end of file +- [New features for plug-in developers](pde.md) + +A special thanks to [everyone who contributed](acknowledgements.md) to this Eclipse release! diff --git a/news/4.x-template/jdt.md b/news/4.x-template/jdt.md index 2e9b7abbb..86f9e89b5 100644 --- a/news/4.x-template/jdt.md +++ b/news/4.x-template/jdt.md @@ -1,5 +1,7 @@ # Java Development Tools - 4.x +A special thanks to everyone who [contributed to JDT](acknowledgements.md#java-development-tools) in this release! + diff --git a/news/4.x-template/platform.md b/news/4.x-template/platform.md index acf37dd13..2ad92efec 100644 --- a/news/4.x-template/platform.md +++ b/news/4.x-template/platform.md @@ -1,5 +1,7 @@ # Platform and Equinox - 4.x +A special thanks to everyone who [contributed to Eclipse-Platform](acknowledgements.md#eclipse-platform) or [contributed to Equinox](acknowledgements.md#equinox) in this release! +