WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 9717d3b

Browse files
authored
Merge pull request #8 from jenkinsci/fix/duplicate-changelog-prevention
Fix/duplicate changelog prevention
2 parents 63cfb69 + 8151cab commit 9717d3b

File tree

3 files changed

+131
-112
lines changed

3 files changed

+131
-112
lines changed

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,46 @@
22

33
All notable changes to the Jenkins Diversion SCM Plugin will be documented in this file.
44

5+
> **Note:** Starting December 2025, versions are identified by commit number rather than semantic versioning.
6+
7+
## 2025-12-09
8+
9+
### Fixed
10+
- **Automatic duplicate commit prevention**: When a library checkout occurs, the plugin now checks if the pipeline's SCM is configured to use the same Diversion repository. If so, the library writes an empty changelog, preventing duplicate commits in the build status grid. This works automatically without requiring any user configuration changes.
11+
12+
### How It Works
13+
During library checkout, the plugin:
14+
1. Accesses the pipeline job via `build.getParent()`
15+
2. Uses reflection to get `CpsScmFlowDefinition` (for "Pipeline script from SCM" jobs)
16+
3. Extracts the configured SCM's repository ID
17+
4. Compares with the library's repository ID
18+
5. If **same repo**: writes empty changelog → prevents duplicates
19+
6. If **different repo**: writes normal changelog → both changelogs shown
20+
21+
### Technical Details
22+
- Added `getPipelineRepositoryId()` method using reflection to access `WorkflowJob.getDefinition().getScm()`
23+
- Library checkout stores repo info and checks pipeline config before writing changelog
24+
- Empty changelog written for same-repo cases: `<changelog></changelog>`
25+
- Console output shows detection: "Library and pipeline use same repository (dv.repo.xxx) - skipping library changelog"
26+
27+
### Tested
28+
- **Environment**: Local Docker Jenkins (jenkins/jenkins:latest)
29+
- **Test case**: Pipeline job with `@Library` from same Diversion repo as pipeline script
30+
- **Before fix**: Build status grid showed "2 commits" (duplicates)
31+
- **After fix**: Build status grid shows "1 commit" (correct)
32+
- **Console verification**:
33+
```
34+
Pipeline is configured with Diversion repository: dv.repo.ce476570-2c46-4fc4-8c4a-541dd6a6d204
35+
Library and pipeline use same repository (dv.repo.ce476570-2c46-4fc4-8c4a-541dd6a6d204) - skipping library changelog to prevent duplicates
36+
```
37+
38+
### Improved
39+
- **README cleanup**: Moved development instructions to DEVELOPMENT.md and version history to CHANGELOG.md
40+
- **Better troubleshooting docs**: Added troubleshooting entry for duplicate commits issue
41+
- **Configuration script**: Updated `configure-diversion-library.groovy` to set `includeInChangesets(false)` by default (as a fallback)
42+
43+
---
44+
545
## [1.0.1] - 2025-11-19
646

747
### Fixed

README.md

Lines changed: 19 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ The plugin will automatically search for a `.groovy` file matching your job name
9696
- **Default Branch**: Branch to checkout (dropdown populates automatically)
9797
- **Library Base Path**: Path to your folder containing `vars/`, `src/`, `resources/` directories (e.g., `Meta/Jenkins/SharedLib`)
9898

99+
#### Important: Prevent Duplicate Commits in Changelog
100+
101+
**If your pipeline script and shared library are in the same Diversion repository**, you should **uncheck** the "Include @Library changes in job recent changes" option. Otherwise, Jenkins will show duplicate commit entries in the build status grid (one from the library checkout and one from the script checkout).
102+
103+
- In the library configuration, uncheck: **"Include @Library changes in job recent changes"**
104+
- Or when configuring via Groovy script, add: `libraryConfig.setIncludeInChangesets(false)`
105+
106+
This is recommended whenever the library is stored in the same repository as your pipeline scripts to avoid confusing duplicate entries.
107+
99108
#### Using the Library in Pipeline Jobs:
100109

101110
```groovy
@@ -163,69 +172,7 @@ This plugin integrates with the [Diversion API](https://docs.diversion.dev/api-r
163172

164173
## Development
165174

166-
### Building the Plugin
167-
168-
```bash
169-
# Clean and compile
170-
mvn clean compile
171-
172-
# Run tests
173-
mvn test
174-
175-
# Package plugin (skips tests for faster builds)
176-
mvn clean package -DskipTests
177-
178-
# Run Jenkins with plugin
179-
mvn hpi:run
180-
```
181-
182-
### Project Structure
183-
184-
```
185-
src/main/java/io/superstudios/plugins/diversion/
186-
├── DiversionSCM.java # Legacy SCM implementation
187-
├── DiversionSCMSource.java # Modern SCM (Global Libraries)
188-
├── DiversionSCMFileSystem.java # File system for library loading
189-
├── DiversionSCMFileSystemBuilder.java # Builder for file systems
190-
├── DiversionSCMHead.java # SCM head (branch) representation
191-
├── DiversionSCMRevision.java # SCM revision (commit) representation
192-
├── DiversionSCMRevisionState.java # State tracking for builds
193-
├── DiversionChangeLogParser.java # Parses changelog XML
194-
├── DiversionChangeLogSet.java # Change log set container
195-
├── DiversionChangeLogEntry.java # Individual changelog entry
196-
├── DiversionApiClient.java # Diversion API client
197-
├── DiversionUIHelper.java # Shared UI helper methods
198-
├── DiversionRepository.java # Repository model
199-
├── DiversionBranch.java # Branch model
200-
├── DiversionCommit.java # Commit model
201-
├── DiversionFile.java # File model
202-
├── DiversionAuthor.java # Author model
203-
└── DiversionTag.java # Tag model (for future use)
204-
205-
src/main/resources/
206-
├── index.jelly # Plugin index page
207-
└── io/superstudios/plugins/diversion/
208-
├── DiversionSCM/
209-
│ ├── config.jelly # Legacy SCM configuration UI
210-
│ └── help-repositoryId.html # Help text
211-
├── DiversionSCMSource/
212-
│ ├── config.jelly # Modern SCM configuration UI
213-
│ └── help-libraryPath.html # Help text
214-
├── DiversionChangeLogEntry/
215-
│ ├── digest.jelly # One-line summary template
216-
│ └── index.jelly # Detail page template
217-
└── DiversionChangeLogSet/
218-
└── index.jelly # ChangeLogSet list template
219-
```
220-
221-
### Code Quality
222-
223-
The plugin follows Jenkins plugin best practices:
224-
- **Code Deduplication**: Shared UI logic in `DiversionUIHelper` class
225-
- **Proper Error Handling**: Graceful fallbacks for API failures
226-
- **Null Safety**: Comprehensive null checks throughout
227-
- **Documentation**: Javadoc comments on all public methods
228-
- **No Stale Code**: All code is actively used
175+
For build instructions, project structure, and contribution guidelines, see [DEVELOPMENT.md](DEVELOPMENT.md).
229176

230177
## Troubleshooting
231178

@@ -256,40 +203,15 @@ The plugin follows Jenkins plugin best practices:
256203
- Check that commits exist in the repository
257204
- Verify the changelog XML file exists in the build directory
258205

259-
## Recent Improvements
260-
261-
### Version 1.0.1
262-
263-
-**Fixed folder-scoped credentials**: Credentials created in folders now work correctly at runtime
264-
-**Credential tracking**: Added credential usage tracking for reporting
265-
-**Modern Credentials API**: Updated to use modern API pattern (no Authentication parameter)
266-
-**UI improvements**: Using `StandardListBoxModel` for proper credential display
267-
- ✅ Updated to Java 17 (matches Jenkins baseline requirements)
268-
- ✅ Migrated to BOM-based dependency management (cleaner, more maintainable)
269-
- ✅ Updated Jenkins baseline to 2.504.3
270-
- ✅ Improved dependency version management using Jenkins BOM
271-
- ✅ Updated parent POM to version 5.28 (Jenkins requirement)
272-
- ✅ Replaced direct dependencies with Jenkins API plugins:
273-
- `httpclient``apache-httpcomponents-client-4-api`
274-
- `jackson-databind``jackson2-api`
275-
- ✅ Added security scanning workflow and dependency update automation
276-
-**Security Enhancements:**
277-
- Credential enumeration protection (permission checks before accessing credentials)
278-
- CSRF protection (`@RequirePOST` annotations on external API calls)
279-
- Proper permission checks using `hasPermission()` for better UX
280-
- Proxy support with authentication via `ProxyConfiguration.newHttpClient()`
281-
- All security findings from Jenkins CodeQL scans resolved
282-
283-
### Version 1.0.0
284-
285-
- ✅ Fixed changelog detail page display
286-
- ✅ Added commit ID display in Changes page list
287-
- ✅ Fixed changelog entry parent relationships
288-
- ✅ Improved global library reload detection (uses commit timestamps)
289-
- ✅ Code deduplication (created `DiversionUIHelper` class)
290-
- ✅ Removed stale code (workspaceId field)
291-
- ✅ Enhanced error handling and null safety
292-
- ✅ Improved Jelly templates with proper null checks
206+
6. **Duplicate Commits in Build Status Grid**
207+
- This happens when your pipeline script and shared library are in the same repository
208+
- Jenkins creates changelog entries for both the script checkout and library checkout
209+
- **Fix**: In the library configuration, uncheck "Include @Library changes in job recent changes"
210+
- Or add `libraryConfig.setIncludeInChangesets(false)` when configuring via Groovy
211+
212+
## Changelog
213+
214+
For version history and recent improvements, see [CHANGELOG.md](CHANGELOG.md).
293215

294216
## Contributing
295217

src/main/java/io/superstudios/plugins/diversion/DiversionSCM.java

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,17 @@ public void checkout(@NonNull Run<?, ?> build, @NonNull Launcher launcher,
220220
}
221221

222222
// Write changelog file if provided
223-
// Use a flag to ensure we only write changelog once per build (prefer script checkout over library)
223+
// Smart duplicate prevention: During library checkout, we check if the pipeline's
224+
// SCM is configured to use the same Diversion repository. If so, we skip writing
225+
// the library changelog to prevent duplicates.
224226
if (changelogFile != null) {
225227
boolean shouldWriteChangelog = false;
228+
boolean skipForSameRepo = false;
229+
230+
// Use build ID to track state across checkouts
231+
String buildKey = build.getExternalizableId();
226232

227-
// Check if this is the first checkout for this build
228233
synchronized (build) {
229-
String changelogKey = "diversion.changelog.written";
230-
Object changelogWritten = build.getAction(hudson.model.ParametersAction.class);
231-
232-
// Use build number + hash as a simple way to track if we've written changelog
233-
String buildKey = build.getExternalizableId();
234234
String alreadyWritten = System.getProperty(buildKey + ".changelog");
235235

236236
if (alreadyWritten == null) {
@@ -241,22 +241,41 @@ public void checkout(@NonNull Run<?, ?> build, @NonNull Launcher launcher,
241241
System.setProperty(buildKey + ".changelog", "script");
242242
listener.getLogger().println("Creating changelog (script checkout)...");
243243
} else {
244-
// Library checkout - only write if it's the first checkout
245-
shouldWriteChangelog = true;
246-
System.setProperty(buildKey + ".changelog", "library");
247-
listener.getLogger().println("Creating changelog (library checkout)...");
244+
// Library checkout - check if pipeline uses the same repo
245+
String pipelineRepoId = getPipelineRepositoryId(build, listener);
246+
if (pipelineRepoId != null && pipelineRepoId.equals(repositoryId)) {
247+
// Same repo! Skip library changelog to prevent duplicates
248+
skipForSameRepo = true;
249+
System.setProperty(buildKey + ".changelog", "library-skipped");
250+
listener.getLogger().println("Library and pipeline use same repository (" + repositoryId + ") - skipping library changelog to prevent duplicates");
251+
} else {
252+
// Different repo or couldn't determine - write changelog
253+
shouldWriteChangelog = true;
254+
System.setProperty(buildKey + ".changelog", "library");
255+
if (pipelineRepoId != null) {
256+
listener.getLogger().println("Library uses different repository than pipeline - creating changelog");
257+
} else {
258+
listener.getLogger().println("Creating changelog (library checkout)...");
259+
}
260+
}
248261
}
249-
} else if (alreadyWritten.equals("library") && !isLibraryCheckout) {
250-
// We wrote changelog for library, but now we have a script checkout
251-
// Overwrite with script checkout (preferred)
262+
} else if ((alreadyWritten.equals("library") || alreadyWritten.equals("library-skipped")) && !isLibraryCheckout) {
263+
// Library checked out first, now script checkout
252264
shouldWriteChangelog = true;
253265
System.setProperty(buildKey + ".changelog", "script");
254-
listener.getLogger().println("Updating changelog (script checkout - preferred over library)...");
266+
listener.getLogger().println("Creating changelog (script checkout)...");
255267
} else {
256268
listener.getLogger().println("Skipping changelog (already written for this build)");
257269
}
258270
}
259271

272+
// Write empty changelog if we're skipping for same-repo
273+
if (skipForSameRepo) {
274+
try (java.io.FileWriter writer = new java.io.FileWriter(changelogFile, java.nio.charset.StandardCharsets.UTF_8)) {
275+
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<changelog>\n</changelog>\n");
276+
}
277+
}
278+
260279
if (shouldWriteChangelog) {
261280
try {
262281
// Get the latest commit for this build
@@ -465,6 +484,44 @@ public hudson.scm.ChangeLogParser createChangeLogParser() {
465484
return new DiversionChangeLogParser();
466485
}
467486

487+
/**
488+
* Get the repository ID configured for the pipeline's SCM (if it's a DiversionSCM).
489+
* This allows us to detect when a library checkout is from the same repo as the pipeline,
490+
* so we can skip writing duplicate changelog entries.
491+
*
492+
* @param build The current build
493+
* @param listener For logging
494+
* @return The repository ID if the pipeline uses DiversionSCM, null otherwise
495+
*/
496+
private String getPipelineRepositoryId(Run<?, ?> build, TaskListener listener) {
497+
try {
498+
Job<?, ?> job = build.getParent();
499+
500+
// Check if this is a WorkflowJob (pipeline)
501+
if (job.getClass().getName().equals("org.jenkinsci.plugins.workflow.job.WorkflowJob")) {
502+
// Use reflection to access the flow definition
503+
java.lang.reflect.Method getDefinition = job.getClass().getMethod("getDefinition");
504+
Object definition = getDefinition.invoke(job);
505+
506+
if (definition != null && definition.getClass().getName().equals("org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition")) {
507+
// This is a "Pipeline script from SCM" job
508+
java.lang.reflect.Method getScm = definition.getClass().getMethod("getScm");
509+
Object scm = getScm.invoke(definition);
510+
511+
if (scm instanceof DiversionSCM) {
512+
String pipelineRepoId = ((DiversionSCM) scm).getRepositoryId();
513+
listener.getLogger().println("Pipeline is configured with Diversion repository: " + pipelineRepoId);
514+
return pipelineRepoId;
515+
}
516+
}
517+
}
518+
} catch (Exception e) {
519+
// Couldn't determine pipeline SCM - not a problem, we'll just write the changelog
520+
listener.getLogger().println("Note: Could not determine pipeline SCM type: " + e.getMessage());
521+
}
522+
return null;
523+
}
524+
468525
/**
469526
* Escape XML special characters
470527
*/

0 commit comments

Comments
 (0)