├── .gitignore ├── .mvn └── maven.config ├── CHANGELOG.adoc ├── CODEOWNERS ├── Jenkinsfile ├── README.adoc ├── pom.xml ├── resources ├── allowed-github-topics.properties ├── artifact-ignores.properties ├── certificates │ ├── README.md │ ├── jenkins-update-center-root-ca-2.crt │ ├── jenkins-update-center-root-ca.crt │ └── jenkins-update-center-root-ca.key.asc ├── deprecations.properties ├── index-template.html ├── label-definitions.properties ├── platform-plugins.json ├── warnings.json └── wiki-overrides.properties ├── site ├── LAYOUT.md ├── README.md ├── generate-htaccess.sh ├── generate.sh ├── publish.sh ├── static │ └── readme.html └── test │ ├── .gitignore │ ├── Dockerfile │ ├── httpd.conf │ └── test.sh ├── spotbugs-excludes.xml └── src ├── assembly.xml ├── main └── java │ └── io │ └── jenkins │ └── update_center │ ├── ArtifactCoordinates.java │ ├── ArtifactoryRepositoryImpl.java │ ├── BaseMavenRepository.java │ ├── DefaultMavenRepositoryBuilder.java │ ├── Deprecations.java │ ├── DirectoryTreeBuilder.java │ ├── GitHubSource.java │ ├── HPI.java │ ├── HealthScores.java │ ├── IndexHtmlBuilder.java │ ├── IndexTemplateProvider.java │ ├── IssueTrackerSource.java │ ├── JenkinsIndexTemplateProvider.java │ ├── JenkinsWar.java │ ├── LatestLinkBuilder.java │ ├── LatestPluginVersions.java │ ├── Main.java │ ├── MaintainersSource.java │ ├── MavenArtifact.java │ ├── MavenRepository.java │ ├── MetadataWriter.java │ ├── Plugin.java │ ├── PluginFilter.java │ ├── PluginUpdateCenterEntry.java │ ├── Popularities.java │ ├── Signer.java │ ├── XmlCache.java │ ├── args4j │ ├── Default.java │ └── LevelOptionHandler.java │ ├── json │ ├── JsonSignature.java │ ├── PlatformCategory.java │ ├── PlatformPlugin.java │ ├── PlatformPluginsRoot.java │ ├── PluginDocumentationUrlsRoot.java │ ├── PluginVersions.java │ ├── PluginVersionsEntry.java │ ├── PluginVersionsRoot.java │ ├── RecentReleasesEntry.java │ ├── RecentReleasesRoot.java │ ├── ReleaseHistoryDate.java │ ├── ReleaseHistoryEntry.java │ ├── ReleaseHistoryRoot.java │ ├── TieredUpdateSitesGenerator.java │ ├── UpdateCenterCore.java │ ├── UpdateCenterDeprecation.java │ ├── UpdateCenterRoot.java │ ├── UpdateCenterWarning.java │ ├── UpdateCenterWarningVersionRange.java │ ├── WithSignature.java │ └── WithoutSignature.java │ ├── util │ ├── Environment.java │ └── Timestamp.java │ └── wrappers │ ├── AllowedArtifactsListMavenRepository.java │ ├── AlphaBetaOnlyRepository.java │ ├── FilteringRepository.java │ ├── MavenRepositoryWrapper.java │ ├── StableWarMavenRepository.java │ ├── TruncatedMavenRepository.java │ └── VersionCappedMavenRepository.java └── test ├── java ├── DummyTest.java ├── ListExisting.java ├── ListPluginsAndVersions.java └── io │ └── jenkins │ └── update_center │ ├── ArtifactCoordinatesTest.java │ ├── DeprecationTest.java │ ├── GitHubSourceTest.java │ ├── HPITest.java │ ├── JsonChecksumTest.java │ ├── MaintainersSourceTest.java │ ├── PluginTest.java │ ├── SanitizerTest.java │ ├── SignerTest.java │ ├── TimestampTest.java │ └── WarningsTest.java └── resources ├── github_graphql_Y3Vyc29yOnYyOpHOA0oRaA==.txt ├── github_graphql_null.txt ├── modern.cert ├── modern.key ├── traditional.cert └── traditional.key /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | /target/ 3 | 4 | # IntelliJ 5 | /.idea/ 6 | /*.iml 7 | /*.ipr 8 | /*.iws 9 | 10 | # Eclipse 11 | /.settings/ 12 | /.classpath 13 | /.project 14 | 15 | # Standard directory names (see site/publish.sh) 16 | /download/ 17 | /www2/ 18 | 19 | # Execution caches dirs 20 | /caches/ 21 | /tmp/ 22 | 23 | # Mac OS X 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | --show-version 2 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | 3 | NOTE: Since version 3.5, changelogs are provided via https://github.com/jenkins-infra/update-center2/releases[GitHub Releases]. 4 | 5 | NOTE: This changelog only covers the actual tool (`update-center2-x.y.z.jar`), not changes to the wrapper scripts or metadata. 6 | 7 | == 3.4.6 (2021-03-01) 8 | 9 | * In case of ambiguous releases (different groups, equivalent but different versions), consistently keep the older one. (https://github.com/jenkins-infra/update-center2/pull/468[#468]) 10 | * Rename `--whitelist-file` to `--allowed-artifacts-file` as part of terminology cleanup. (https://github.com/jenkins-infra/update-center2/pull/482[#482]) 11 | 12 | == 3.4.5 (2020-09-24) 13 | 14 | * Add `target="_blank"` for all links in plugin descriptions. (https://github.com/jenkins-infra/update-center2/pull/446[#446]) 15 | 16 | == 3.4.4 (2020-09-22) 17 | 18 | * Allow overriding the minimum validity duration using the Java system property `CERTIFICATE_MINIMUM_VALID_DAYS`. 19 | (https://github.com/jenkins-infra/update-center2/pull/448[#448], https://issues.jenkins.io/browse/INFRA-2732[INFRA-2732]) 20 | 21 | == 3.4.3 (2020-09-10) 22 | 23 | * When determining tiers, create a tier for the next weekly after an LTS baseline when there are tiers for that LTS line's releases. (https://github.com/jenkins-infra/update-center2/pull/443[#443]) 24 | 25 | == 3.4.2 (2020-08-22) 26 | 27 | * Reduce the duration for which releases are considered _recent_ for `recent-releases.json` from 24 to 3 hours. (https://github.com/jenkins-infra/update-center2/pull/433[#433]) 28 | 29 | == 3.4.1 (2020-08-21) 30 | 31 | * Also consider the Java system property `DOWNLOADS_ROOT_URL` for war download URLs, not just plugins. (https://github.com/jenkins-infra/update-center2/pull/430[#430]) 32 | 33 | == 3.4 (2020-08-21) 34 | 35 | * Provide `recent-releases.json` to selectively sync new files to mirrors. (https://github.com/jenkins-infra/update-center2/pull/428[#428]) 36 | * Identify which plugins have invalid (non-existing) core dependencies. (https://github.com/jenkins-infra/update-center2/pull/417[#417]) 37 | * Close a `Reader`. (https://github.com/jenkins-infra/update-center2/pull/413[#413]) 38 | * Javadoc fixes. (https://github.com/jenkins-infra/update-center2/pull/414[#414]) 39 | 40 | == 3.3.1 (2020-08-01) 41 | 42 | * Fix the URL we obtain the jar icon in generated directory listings from. (https://github.com/jenkins-infra/update-center2/pull/409[#409]) 43 | * Further standardize on `fastjson`, remove `gson`. (https://github.com/jenkins-infra/update-center2/pull/394[#394]) 44 | 45 | == 3.3 (2020-06-25) 46 | 47 | * Dynamically determine update site tiers based on plugin dependencies. 48 | (https://github.com/jenkins-infra/update-center2/pull/376[#376], https://issues.jenkins.io/browse/INFRA-2615[INFRA-2615], https://issues.jenkins.io/browse/INFRA-1021[INFRA-1021]) 49 | 50 | == 3.2.1 (2020-06-06) 51 | 52 | * Fix XXE vulnerability when processing `pom.xml` files. (https://github.com/jenkins-infra/update-center2/commit/f207cfb0025017c9a525c57cdadb8416ee2d27c3[f207cfb0]) 53 | 54 | == 3.2 (2020-05-29) 55 | 56 | * Add information about what the latest version of a plugin is, even if not offered for installation. 57 | (https://github.com/jenkins-infra/update-center2/pull/382[#382], https://issues.jenkins.io/browse/JENKINS-62332[JENKINS-62332]) 58 | 59 | == 3.1 (2020-05-26) 60 | 61 | * Add separate, configurable `deprecations` metadata section to `update-center.json`. 62 | (https://github.com/jenkins-infra/update-center2/pull/344[#344], https://issues.jenkins.io/browse/JENKINS-59136[JENKINS-59136]) 63 | 64 | == 3.0.1 (2020-05-23) 65 | 66 | * Javadoc fixes. (https://github.com/jenkins-infra/update-center2/commit/fe8b8e09c20cddf578377cb0e9873e5604bd7a8d[fe8b8e09]) 67 | 68 | == 3.0 (2020-05-23) 69 | 70 | * **Major Overhaul** (https://github.com/jenkins-infra/update-center2/pull/365[#365]) 71 | * Add mode that uses Artifactory API instead of Nexus Maven indexes (https://github.com/jenkins-infra/update-center2/pull/364[#364]) 72 | * Add plugin popularity metadata. (https://github.com/jenkins-infra/update-center2/pull/356[#356], #369) 73 | * Use GitHub repository URL for plugins that do not specify a URL, or have an obviously wrong one. (https://github.com/jenkins-infra/update-center2/pull/335[#335]) 74 | * Prefer URL in plugin manifest to `url` in `pom.xml`. (https://github.com/jenkins-infra/update-center2/pull/303[#303], https://issues.jenkins.io/browse/INFRA-2292[INFRA-2292]) 75 | 76 | Version 3.0 is the first version that was not just recompiled by the wrapper script on every execution. 77 | Before this release, this tool was essentially unversioned. 78 | The changes listed above include all substantial changes to the tool since 2018. 79 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | /site/publish.sh @jenkins-infra/core 2 | /resources/warnings.json @jenkins-infra/security 3 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | properties([ 4 | disableConcurrentBuilds(), 5 | buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '2')), 6 | ]) 7 | 8 | node('linux') { 9 | stage('Prepare') { 10 | deleteDir() 11 | checkout scm 12 | } 13 | 14 | stage('Generate') { 15 | withEnv([ 16 | "PATH+MVN=${tool 'mvn'}/bin", 17 | "JAVA_HOME=${tool 'jdk21'}", 18 | "PATH+JAVA=${tool 'jdk21'}/bin" 19 | ]) { 20 | sh 'mvn -e clean verify' 21 | } 22 | } 23 | 24 | stage('Archive Test Report') { 25 | archive 'target/surefire-reports/*-output.txt' 26 | } 27 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci 5 | jenkins 6 | 1.131 7 | 8 | 9 | update-center2 10 | 11 | 3.18.3-SNAPSHOT 12 | Jenkins Update Center Generator 13 | Generates update sites for updates.jenkins.io 14 | 15 | 16 | 21 17 | io.jenkins.update_center.Main 18 | false 19 | Low 20 | UTF-8 21 | UTF-8 22 | UTF-8 23 | 1.80 24 | 25 | 26 | 27 | 28 | 29 | maven-jar-plugin 30 | 31 | 32 | 33 | true 34 | ${main.class} 35 | 36 | 37 | 38 | 39 | 40 | maven-enforcer-plugin 41 | 42 | 43 | display-info 44 | validate 45 | 46 | enforce 47 | 48 | 49 | 50 | 51 | ${maven.compiler.release} 52 | 53 | test 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | maven-assembly-plugin 63 | 64 | 65 | 66 | single 67 | 68 | package 69 | 70 | 71 | src/assembly.xml 72 | 73 | 74 | 75 | 76 | 77 | 78 | org.codehaus.mojo 79 | appassembler-maven-plugin 80 | 2.1.0 81 | 82 | 83 | 84 | ${main.class} 85 | app 86 | 87 | 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-surefire-plugin 93 | 3.5.3 94 | 95 | true 96 | 97 | 98 | 99 | com.github.spotbugs 100 | spotbugs-maven-plugin 101 | 4.9.3.0 102 | ${project.basedir}/spotbugs-excludes.xml 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.jvnet.hudson 110 | crypto-util 111 | 1.0 112 | 113 | 114 | com.alibaba 115 | fastjson 116 | 117 | 2.0.57 118 | 119 | 120 | org.dom4j 121 | dom4j 122 | 2.1.4 123 | 124 | 125 | jaxen 126 | jaxen 127 | 2.0.0 128 | 129 | 130 | org.kohsuke.stapler 131 | json-lib 132 | 2.4-jenkins-8 133 | 134 | 135 | commons-io 136 | commons-io 137 | 2.18.0 138 | 139 | 140 | org.apache.commons 141 | commons-lang3 142 | 3.17.0 143 | 144 | 145 | commons-codec 146 | commons-codec 147 | 1.18.0 148 | 149 | 150 | args4j 151 | args4j 152 | 2.37 153 | 154 | 155 | io.jenkins.lib 156 | support-log-formatter 157 | 1.3 158 | 159 | 160 | org.jenkins-ci 161 | version-number 162 | 1.12 163 | 164 | 165 | org.bouncycastle 166 | bcprov-jdk18on 167 | ${bouncycastle.version} 168 | 169 | 170 | org.bouncycastle 171 | bcpkix-jdk18on 172 | ${bouncycastle.version} 173 | 174 | 175 | org.apache.ant 176 | ant 177 | 1.10.15 178 | 179 | 180 | com.googlecode.owasp-java-html-sanitizer 181 | owasp-java-html-sanitizer 182 | 20240325.1 183 | 184 | 185 | com.github.spotbugs 186 | spotbugs-annotations 187 | 188 | compile 189 | 190 | 191 | org.jsoup 192 | jsoup 193 | 1.19.1 194 | 195 | 196 | 197 | 198 | org.hamcrest 199 | hamcrest 200 | 3.0 201 | test 202 | 203 | 204 | junit 205 | junit 206 | 4.13.2 207 | test 208 | 209 | 210 | com.squareup.okhttp3 211 | mockwebserver 212 | 4.12.0 213 | test 214 | 215 | 216 | 217 | 218 | 219 | repo.jenkins-ci.org 220 | https://repo.jenkins-ci.org/public/ 221 | 222 | 223 | 224 | 225 | scm:git:git://github.com/jenkins-infra/update-center2.git 226 | scm:git:ssh://git@github.com/jenkins-infra/update-center2.git 227 | HEAD 228 | 229 | 230 | 231 | 232 | maven.jenkins-ci.org 233 | https://repo.jenkins-ci.org/releases 234 | 235 | 236 | maven.jenkins-ci.org 237 | https://repo.jenkins-ci.org/snapshots 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /resources/allowed-github-topics.properties: -------------------------------------------------------------------------------- 1 | # This file lists all the labels that can be used via GitHub repository topics 2 | 3 | # Plugin governance labels. https://jenkins.io/doc/developer/plugin-governance/ 4 | adopt-this-plugin 5 | deprecated 6 | 7 | # Plugin categorization 8 | administrative-monitor 9 | agent 10 | analysis 11 | android 12 | api-plugin 13 | aws 14 | azure 15 | bamboo 16 | bitbucket 17 | bitbucket-server 18 | builder 19 | buildwrapper 20 | cli 21 | cloud 22 | cluster 23 | cmp 24 | codebuild 25 | confluence 26 | configuration-as-code 27 | credentials 28 | database 29 | deployment 30 | devops 31 | devsecops 32 | dotnet 33 | docker 34 | email 35 | emailext 36 | external 37 | fingerprint 38 | git 39 | github 40 | gitlab 41 | groovy-related 42 | google-cloud 43 | ios 44 | jira 45 | kubernetes 46 | library 47 | listview-column 48 | logging 49 | localization 50 | maven 51 | monitoring 52 | misc 53 | notification 54 | notifier 55 | npm 56 | observability 57 | openshift 58 | orchestration 59 | page-decorator 60 | parameter 61 | performance 62 | pipeline 63 | post-build 64 | python 65 | queue 66 | redis 67 | report 68 | ruby 69 | runcondition 70 | scm 71 | scm-related 72 | security 73 | slack 74 | slaves 75 | spotinst 76 | stash 77 | test 78 | theme 79 | trigger 80 | ui 81 | upload 82 | user 83 | view 84 | webhook 85 | website 86 | windows 87 | -------------------------------------------------------------------------------- /resources/certificates/README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Update Center Root CA 2 | 3 | ## `jenkins-update-center-root-ca` 4 | 5 | This certificate and private key is used to generate another keypair, which is used to sign update center metadata. 6 | It is included in [Jenkins 1.410 and newer][src] as a trust anchor. 7 | 8 | This certificate is valid from 2011-04-19 to 2021-04-16. 9 | 10 | The corresponding private key in this directory is PGP encrypted by the board key used to handle CLAs: 11 | 12 | ```` 13 | pub 4096R/6E33EEFA 2012-03-21 14 | uid Jenkins project CLA (Used to encrypt Jenkins CLA papers) 15 | uid [jpeg image of size 11091] 16 | sub 4096R/FDDFA9FC 2012-03-21 17 | ```` 18 | 19 | 20 | ## `jenkins-update-center-root-ca-2` 21 | 22 | This certificate is a replacement for `jenkins-update-center-root-ca` and has been added to Jenkins in April 2018 for [INFRA-1502][INFRA-1502]. 23 | It is included in Jenkins 2.117 and newer as a trust anchor. 24 | 25 | This certificate is valid from 2018-04-08 to 2028-04-05. 26 | 27 | ### Key owners 28 | As of May 2020, Kohsuke, Oleg Nenashev, and Olivier Vernin have the corresponding private key. 29 | 30 | [INFRA-1502]: https://issues.jenkins-ci.org/browse/INFRA-1502 31 | [src]: https://github.com/jenkinsci/jenkins/blob/f5ac512bd4e6d3bf041672d179a97f8dfd900e8b/war/src/main/webapp/WEB-INF/update-center-rootCAs/jenkins-update-center-root-ca 32 | -------------------------------------------------------------------------------- /resources/certificates/jenkins-update-center-root-ca-2.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 33 | T1JjZA8VNmdlKiPqf4S9u/kTAojeLB9zU0cTSW0= 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /resources/certificates/jenkins-update-center-root-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEjDCCA3SgAwIBAgIJANhML3taKQRDMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD 3 | VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEBxMIU2FuIEpvc2Ux 4 | GDAWBgNVBAoTD0plbmtpbnMgUHJvamVjdDEaMBgGA1UEAxMRS29oc3VrZSBLYXdh 5 | Z3VjaGkxHTAbBgkqhkiG9w0BCQEWDmtrQGtvaHN1a2Uub3JnMB4XDTExMDQxOTA2 6 | NDMyOVoXDTIxMDQxNjA2NDMyOVowgYoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD 7 | YWxpZm9ybmlhMREwDwYDVQQHEwhTYW4gSm9zZTEYMBYGA1UEChMPSmVua2lucyBQ 8 | cm9qZWN0MRowGAYDVQQDExFLb2hzdWtlIEthd2FndWNoaTEdMBsGCSqGSIb3DQEJ 9 | ARYOa2tAa29oc3VrZS5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 10 | AQC11JA72km0SyShPrAw2bfwePXk7GofoRHSiBnALif80L3NrlXvM5oLmP/T9I9i 11 | ZX/sYmMljOGyFrHK66SOEPoOkXvBFpAJxDDUXo/WTb0s7hUI0O+Hj5q8zPiTOTya 12 | +L5pRK+Am699Oq9l4yrXkLwAEtsDYYx+u+YCYBO3gohUsVCGVt9bGbPLoCK0EkSR 13 | i6agGaDrdcctfWPxhMiAGOcjetPRJSRo2Ah4zeXCBK8mLgP0oLX3XebNG13jcFqs 14 | Jpkzzk3KR0+PXbHgjju31z/XC6mSoB2tyzDdgY3xRYc0ckNdmpl6S9V0GWXdR/2B 15 | /HRnP2tbOSPxIyTkkxp4G1YvAgMBAAGjgfIwge8wHQYDVR0OBBYEFNVa3x15M77T 16 | HUAQv6LJ7+YE3OSKMIG/BgNVHSMEgbcwgbSAFNVa3x15M77THUAQv6LJ7+YE3OSK 17 | oYGQpIGNMIGKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8G 18 | A1UEBxMIU2FuIEpvc2UxGDAWBgNVBAoTD0plbmtpbnMgUHJvamVjdDEaMBgGA1UE 19 | AxMRS29oc3VrZSBLYXdhZ3VjaGkxHTAbBgkqhkiG9w0BCQEWDmtrQGtvaHN1a2Uu 20 | b3JnggkA2Ewve1opBEMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA 21 | CrUdQReyKL6zStkm21wQcYIMStT56o5Zo1cN0KKXyELq3vayRfrOGOZ6lStDbnlM 22 | crxD0bl7+S+BpKTrEvV+fokRUi3wH4yGgI6LBo+VzN7l0u/5WltjydfZqjne+gUe 23 | D8R0puZGJ+3yGjLK1hz5vzuSpZtwQb5l9Jv8rZ4YMIZsRLawjG8vxCFEHL6ZseI5 24 | Ezwa25fqPp4at2siOj8yhIygSEXOe5q6ivdZJf/AtkAZhSHuXhXACYFW15zxNyxv 25 | FNWPgAj1UFEXFfW7+2UmMwyJg4n8qPauCF5BhmqWKBwkFBKcM4rllmSohbJO8gar 26 | vj3KU0++qx9tZVvTYHPFIQ== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /resources/certificates/jenkins-update-center-root-ca.key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP MESSAGE----- 2 | Version: GnuPG v1 3 | 4 | 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 44 | =Ly1b 45 | -----END PGP MESSAGE----- 46 | -------------------------------------------------------------------------------- /resources/deprecations.properties: -------------------------------------------------------------------------------- 1 | # This file contains deprecated plugins (keys) and the URL the deprecation notice on plugins.jenkins.io and in Jenkins 2 | # should link to (value). Use this file to mark a plugin as deprecated while continuing to distribute it. If a plugin 3 | # should be removed from distribution entirely, instead set a deprecation notice URL in artifact-ignores.properties. 4 | 5 | async-http-client = https://github.com/jenkins-infra/update-center2/pull/650 6 | BlameSubversion = https://github.com/jenkinsci/jenkins/pull/5320 7 | blueocean-executor-info = https://issues.jenkins.io/browse/JENKINS-56773 8 | bmc-rpd = https://github.com/jenkins-infra/update-center2/pull/833 9 | bmc-change-manager-imstm = https://github.com/jenkinsci/bmc-change-manager-imstm-plugin/pull/13 10 | chosen = https://github.com/jenkins-infra/update-center2/pull/833 11 | cloverphp = https://github.com/jenkinsci/jenkins/pull/5320 12 | cmvc = https://github.com/jenkinsci/jenkins/pull/5320 13 | coding-webhook = https://github.com/jenkinsci/coding-webhook-plugin/commit/20e1449513628ad24476b47331ea7bd6a2e82583 14 | container-image-link = https://issues.jenkins.io/browse/JENKINS-75608 15 | covcomplplot = https://github.com/jenkins-infra/update-center2/pull/833 16 | dynatrace-dashboard = https://github.com/jenkins-infra/update-center2/pull/531/ 17 | elastest = https://www.jenkins.io/blog/2021/11/09/guava-upgrade/ 18 | elasticbox = https://github.com/jenkins-infra/update-center2/pull/833 19 | emma = https://github.com/jenkinsci/jenkins/pull/5320 20 | extended-choice-parameter = https://github.com/jenkinsci/extended-choice-parameter-plugin?tab=readme-ov-file#end-of-life 21 | extreme-feedback = https://www.jenkins.io/blog/2021/11/09/guava-upgrade/ 22 | google-cloud-health-check = https://www.jenkins.io/blog/2021/11/09/guava-upgrade/ 23 | harvest = https://github.com/jenkinsci/jenkins/pull/5320 24 | greenballs = https://github.com/jenkins-infra/update-center2/pull/735 25 | itms-for-jira = https://github.com/jenkinsci/itms-for-jira-plugin/pull/5 26 | javatest-report = https://github.com/jenkinsci/jenkins/pull/5320 27 | jobgenerator = https://github.com/jenkins-infra/update-center2/pull/795 28 | mesos = https://github.com/jenkins-infra/update-center2/pull/865 29 | multi-slave-config-plugin = https://github.com/jenkins-infra/update-center2/pull/729 30 | nis-notification-lamp = https://github.com/jenkinsci/jenkins/pull/5521 31 | openshift-deployer = https://www.jenkins.io/blog/2021/11/09/guava-upgrade/ 32 | performance-signature-dynatrace = https://github.com/jenkins-infra/update-center2/pull/531 33 | promoted-builds-simple = https://github.com/jenkins-infra/update-center2/pull/833 34 | read-only-configurations = https://github.com/jenkins-infra/update-center2/pull/795 35 | recipe = https://github.com/jenkins-infra/update-center2/pull/833 36 | relution-publisher = https://www.jenkins.io/blog/2021/11/09/guava-upgrade/ 37 | rusalad = https://github.com/jenkins-infra/update-center2/pull/833 38 | scm-httpclient = https://www.jenkins.io/blog/2021/11/09/guava-upgrade/ 39 | scp = https://github.com/jenkinsci/scp-plugin/blob/master/README.md 40 | sicci_for_xcode = https://github.com/jenkinsci/jenkins/pull/5560 41 | slave-prerequisites = https://github.com/jenkinsci/jenkins/pull/5526 42 | synergy = https://github.com/jenkinsci/jenkins/pull/5320 43 | testabilityexplorer = https://github.com/jenkins-infra/update-center2/pull/833 44 | tmpcleaner = https://github.com/jenkinsci/jenkins/pull/5560 45 | translation = https://github.com/jenkins-infra/update-center2/pull/719 46 | vertx = https://github.com/jenkinsci/jenkins/pull/5526 47 | vs-code-metrics = https://github.com/jenkinsci/jenkins/pull/5320 48 | -------------------------------------------------------------------------------- /resources/index-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ title }} 4 | 5 | 6 | 50 | 51 | 52 |
53 |

{{ title }}

54 |
{{ subtitle }}
55 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /resources/platform-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category":"Organization and Administration", 4 | "plugins": [ 5 | { "name": "dashboard-view" }, 6 | { "name": "cloudbees-folder", "suggested": true }, 7 | { "name": "configuration-as-code" }, 8 | { "name": "antisamy-markup-formatter", "suggested": true } 9 | ] 10 | }, 11 | { 12 | "category":"Build Features", 13 | "description":"Add general purpose features to your jobs", 14 | "plugins": [ 15 | { "name": "build-name-setter" }, 16 | { "name": "build-timeout", "suggested": true }, 17 | { "name": "config-file-provider" }, 18 | { "name": "credentials-binding", "suggested": true }, 19 | { "name": "embeddable-build-status" }, 20 | { "name": "rebuild" }, 21 | { "name": "ssh-agent" }, 22 | { "name": "throttle-concurrents" }, 23 | { "name": "timestamper", "suggested": true }, 24 | { "name": "ws-cleanup", "suggested": true } 25 | ] 26 | }, 27 | { 28 | "category":"Build Tools", 29 | "plugins": [ 30 | { "name": "ant", "suggested": true }, 31 | { "name": "gradle", "suggested": true }, 32 | { "name": "msbuild" }, 33 | { "name": "nodejs" } 34 | ] 35 | }, 36 | { 37 | "category":"Build Analysis and Reporting", 38 | "plugins": [ 39 | { "name": "cobertura" }, 40 | { "name": "htmlpublisher" }, 41 | { "name": "junit" }, 42 | { "name": "warnings-ng" }, 43 | { "name": "xunit" } 44 | ] 45 | }, 46 | { 47 | "category":"Pipelines and Continuous Delivery", 48 | "plugins": [ 49 | { "name": "workflow-aggregator", "suggested": true, "added": "2.0" }, 50 | { "name": "github-branch-source", "suggested": true, "added": "2.0" }, 51 | { "name": "pipeline-github-lib", "suggested": true, "added": "2.0" }, 52 | { "name": "pipeline-stage-view", "suggested": true, "added": "2.0" }, 53 | { "name": "conditional-buildstep" }, 54 | { "name": "jenkins-multijob-plugin" }, 55 | { "name": "parameterized-trigger" }, 56 | { "name": "copyartifact" } 57 | ] 58 | }, 59 | { 60 | "category":"Source Code Management", 61 | "plugins": [ 62 | { "name": "bitbucket" }, 63 | { "name": "clearcase" }, 64 | { "name": "cvs" }, 65 | { "name": "git", "suggested": true }, 66 | { "name": "git-parameter" }, 67 | { "name": "github" }, 68 | { "name": "gitlab-plugin" }, 69 | { "name": "p4" }, 70 | { "name": "repo" }, 71 | { "name": "subversion" } 72 | ] 73 | }, 74 | { 75 | "category":"Distributed Builds", 76 | "plugins": [ 77 | { "name": "matrix-project" }, 78 | { "name": "ssh-slaves", "suggested": true }, 79 | { "name": "windows-slaves" } 80 | ] 81 | }, 82 | { 83 | "category":"User Management and Security", 84 | "plugins": [ 85 | { "name": "matrix-auth", "suggested": true }, 86 | { "name": "pam-auth", "suggested": true }, 87 | { "name": "ldap", "suggested": true }, 88 | { "name": "role-strategy" }, 89 | { "name": "active-directory" }, 90 | { "name": "authorize-project" } 91 | ] 92 | }, 93 | { 94 | "category":"Notifications and Publishing", 95 | "plugins": [ 96 | { "name": "email-ext", "suggested": true }, 97 | { "name": "emailext-template" }, 98 | { "name": "mailer", "suggested": true }, 99 | { "name": "publish-over-ssh" }, 100 | { "name": "ssh" } 101 | ] 102 | }, 103 | { 104 | "category":"Languages", 105 | "plugins": [ 106 | { "name": "locale"}, 107 | { "name": "localization-zh-cn"} 108 | ] 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /resources/wiki-overrides.properties: -------------------------------------------------------------------------------- 1 | # This file overrides the inference of plugin -> Wiki page link 2 | # Meant to be a temporary measure until the plugin POM has the correct URL field. 3 | 4 | # Required until a newer version than 1.1 is released 5 | archived-artifact-url-viewer=https://wiki.jenkins-ci.org/display/JENKINS/Archived+Artifact+Url+Viewer+PlugIn 6 | 7 | # Required until a newer version than 1.1.0 is released 8 | skype-notifier=http://wiki.jenkins-ci.org/display/JENKINS/Skype+Plugin 9 | 10 | # Required until a new version with this URL in pom.xml is released 11 | ApicaLoadtest=https://wiki.jenkins-ci.org/display/JENKINS/Apica+Loadtest+Plugin 12 | chef-tracking=https://wiki.jenkins-ci.org/display/JENKINS/Chef+Tracking+Plugin 13 | ## Macro generates a wrong link, source is https://github.com/jenkinsci/chosen-views-tabbar 14 | chosen-views-tabbar=https://wiki.jenkins-ci.org/display/JENKINS/Chosen+Views+Tab+Bar 15 | ## Source seems to *not* be under jenkinsci org: https://github.com/ThoughtsOnMobile/cocoapods-jenkins-integration 16 | cocoapods-integration=https://wiki.jenkins-ci.org/display/JENKINS/CocoaPods+Plugin 17 | custom-view-tabs=https://wiki.jenkins-ci.org/display/JENKINS/Custom+View+Tabs+Plugin 18 | ## Currently has the tag, BUT with a typo https://wiki.jenkins-ci.org/display/JENKINS/Deploment+Notification+Plugin 19 | deployment-notification=https://wiki.jenkins-ci.org/display/JENKINS/Deployment+Notification+Plugin 20 | database-drizzle=https://wiki.jenkins-ci.org/display/JENKINS/Drizzle(+MySQL)%20Database%20Plugin 21 | diskcheck=https://wiki.jenkins-ci.org/display/JENKINS/DiskCheck+Plugin 22 | dynamicparameter=https://wiki.jenkins-ci.org/display/JENKINS/Dynamic+Parameter+Plug-in 23 | figlet-buildstep=https://wiki.jenkins-ci.org/display/JENKINS/Figlet+plugin 24 | groovyaxis=https://wiki.jenkins-ci.org/display/JENKINS/GroovyAxis 25 | hckrnews=https://wiki.jenkins-ci.org/display/JENKINS/Hckrnews+Plugin 26 | icon-shim=https://wiki.jenkins-ci.org/display/JENKINS/Icon+Shim+Plugin 27 | jenkins-testswarm-plugin=https://wiki.jenkins-ci.org/display/JENKINS/testswarm-plugin 28 | job-direct-mail=https://wiki.jenkins-ci.org/display/JENKINS/Job+Direct+Mail+Plugin 29 | jython=https://wiki.jenkins-ci.org/display/JENKINS/Jython+Plugin 30 | m2-repo-reaper=http://wiki.jenkins-ci.org/display/JENKINS/M2+Repository+Cleanup+Plugin 31 | mailmap-resolver=https://wiki.jenkins-ci.org/display/JENKINS/Mail+Map+Resolver 32 | mansion-cloud=https://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Cloud+Connector+Plugin 33 | ## Currently has the tag, BUT with a typo https://wiki.jenkins-ci.org/display/JENKINS/OpenJDK+native+intaller+plugin 34 | openJDK-native-plugin=https://wiki.jenkins-ci.org/display/JENKINS/OpenJDK+native+installer+plugin 35 | periodicbackup=https://wiki.jenkins-ci.org/display/JENKINS/PeriodicBackup+Plugin 36 | php=https://wiki.jenkins-ci.org/display/JENKINS/PHP+Plugin 37 | play-autotest-plugin=https://wiki.jenkins.io/display/JENKINS/Play+Framework+Plugin 38 | ## Sources are not under jenkinsci, but at https://bitbucket.org/henriklynggaard/prereq-buildstep-plugin/ 39 | prereq-buildstep=http://wiki.jenkins-ci.org/display/JENKINS/Prerequisite+build+step+plugin 40 | ## Sources are not under jenkinsci, but at https://bitbucket.org/henriklynggaard/project-health-report-plugin/ 41 | project-health-report=https://wiki.jenkins-ci.org/display/JENKINS/Project+Health+Report+Plugin 42 | puppet=https://wiki.jenkins-ci.org/display/JENKINS/Puppet+Plugin 43 | ruby-runtime=https://wiki.jenkins-ci.org/display/JENKINS/Ruby+Runtime+Plugin 44 | started-by-envvar=https://wiki.jenkins-ci.org/display/JENKINS/Started-By+Environment+Variable+Plugin 45 | StashBranchParameter=https://wiki.jenkins-ci.org/display/JENKINS/StashBranchParameter 46 | # Macro leads to 404. Seems like the sources are under https://github.com/davidparsson/svn-workspace-cleaner-plugin (i.e. not forked) 47 | svn-workspace-cleaner=https://wiki.jenkins-ci.org/display/JENKINS/SVN+Workspace+Cleaner 48 | text-finder-run-condition=https://wiki.jenkins-ci.org/display/JENKINS/Text+Finder+Run+Condition+Plugin 49 | unicorn=http://wiki.jenkins-ci.org/display/JENKINS/Unicorn+Validation+Plugin 50 | vaddy-plugin=https://wiki.jenkins-ci.org/display/JENKINS/VAddy+Plugin 51 | violations=https://wiki.jenkins-ci.org/display/JENKINS/Violations 52 | xpdev=https://wiki.jenkins-ci.org/display/JENKINS/XP-Dev+Plugin 53 | # wiki doesn't actually have any content 54 | working-hours=https://github.com/jenkinsci/working-hours-plugin 55 | 56 | ca-apm=https://wiki.jenkins.io/display/JENKINS/CA+APM+Plugin+1.x 57 | comments-remover=https://wiki.jenkins.io/display/JENKINS/XComment.io+-+Comments+Remover+Plugin 58 | pipeline-model-declarative-agent=https://github.com/jenkinsci/pipeline-model-definition-plugin/blob/master/pipeline-model-declarative-agent/README.md 59 | 60 | # Deprecated plugins (replaced by warnings-ng) 61 | pmd=https://github.com/jenkinsci/pmd-plugin 62 | dry=https://github.com/jenkinsci/dry-plugin 63 | findbugs=https://github.com/jenkinsci/findbugs-plugin 64 | analysis-core=https://github.com/jenkinsci/analysis-core-plugin 65 | analysis-collector=https://github.com/jenkinsci/analysis-collector-plugin 66 | warnings=https://github.com/jenkinsci/warnings-plugin 67 | tasks=https://github.com/jenkinsci/tasks-plugin 68 | checkstyle=https://github.com/jenkinsci/checkstyle-plugin 69 | android-lint=https://github.com/jenkinsci/android-lint-plugin 70 | 71 | # Other deprecated plugins 72 | github-organization-folder=https://github.com/jenkinsci/github-organization-folder-plugin 73 | cloudbees-plugin-gateway=https://github.com/jenkinsci/cloudbees-plugin-gateway 74 | 75 | # JS Libs plugins 76 | jquery-detached=https://github.com/jenkinsci/js-libs/blob/master/jquery-detached/README.md 77 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Update Center Site Architecture 2 | This script generates the code and data behind https://updates.jenkins.io/ 3 | 4 | The service this website provides is as follows: 5 | 6 | 1. Jenkins will hit well-known URLs hard-coded into Jenkins binaries with 7 | the Jenkins version number attached as a query string, like 8 | https://updates.jenkins.io/update-center.json?version=1.345 9 | 10 | 1. We use the version number to redirect the traffic to the right update site, 11 | among all the ones that we generate. 12 | 13 | 14 | ## Multiple update sites for different version ranges 15 | 16 | Update center metadata can contain only one version per a plugin. 17 | Because newer versions of the same plugin may depend on newer version of Jenkins, if we just serve one update center for every Jenkins out there, older versions of Jenkins will see plugin versions that do not work with them, making it impossible to install the said plugin. 18 | This is unfortunate because some younger versions of the plugin might have worked with that Jenkins core. 19 | This creates a disincentive for plugin developers to move to the new base version, and slows down the pace in which it adopts new core features. 20 | 21 | So we generate several update centers targeted for different version ranges. 22 | 23 | To accommodate all recent Jenkins releases, we first inspect all plugin releases for their Jenkins core dependencies. 24 | We then generate tiered update sites for all releases identified this way that are more recent than a cutoff (~3 months). 25 | Directories containing these tiered update sites have the prefix `dynamic-`. 26 | 27 | mod_rewrite rules in an `.htaccess` file are then used to redirect requests from Jenkins versions to the next lower update site. 28 | It will serve the newest release of each plugin that is compatible with the specified Jenkins version. 29 | See [generate-htaccess.sh](generate-htaccess.sh) for how these rules are generated. 30 | 31 | ## Generating update sites 32 | 33 | [generate.sh](generate.sh) is run by [a CI job](https://trusted.ci.jenkins.io/job/update_center/) 34 | and generates all the different sites as static files, and deploys the directory into Apache. 35 | 36 | A part of this is [.htaccess](static/.htaccess) that uses `mod_rewrite` to 37 | redirect inbound requests to the right version specific website. 38 | 39 | 40 | ## Layout 41 | 42 | See [a separate doc](LAYOUT.md) for the layout of the generated update site. 43 | -------------------------------------------------------------------------------- /site/generate-htaccess.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USAGE="Usage: $0 [ ...] 4 | " 5 | 6 | [[ $# -gt 0 ]] || { echo "${USAGE}Expected at least one argument." >&2 ; exit 1 ; } 7 | 8 | set -o pipefail 9 | set -o errexit 10 | set -o nounset 11 | 12 | cat < 16 | RewriteEngine on 17 | 18 | # https://github.com/jenkinsci/jenkins/blob/56b6623915c50b4a2ef994a143a8fe0829587f3c/core/src/main/java/hudson/model/UpdateCenter.java#L1182-L1207 19 | RewriteCond %{QUERY_STRING} ^.*uctest$ 20 | RewriteRule ^(|.+/)(update\-center.*\.(json|html)+) /uctest.json [NC,L] 21 | EOF 22 | 23 | echo "# Version-specific rulesets generated by generate.sh" 24 | n=$# 25 | versions=( "$@" ) 26 | newestStable= 27 | oldestStable= 28 | oldestWeekly= 29 | 30 | for (( i = n-1 ; i >= 0 ; i-- )) ; do 31 | version="${versions[i]}" 32 | IFS=. read -ra versionPieces <<< "$version" 33 | 34 | major=${versionPieces[0]} 35 | minor=${versionPieces[1]} 36 | patch= 37 | if [[ ${#versionPieces[@]} -gt 2 ]] ; then 38 | patch=${versionPieces[2]} 39 | fi 40 | 41 | if [[ "$version" =~ ^2[.][0-9]+[.][0-9]$ ]] ; then 42 | # This is an LTS version 43 | if [[ -z "$newestStable" ]] ; then 44 | newestStable="$version" 45 | fi 46 | 47 | cat < ${major} or major = ${major} and minor > ${minor} or major = ${major} and minor = ${minor} and patch >= ${patch}, use this LTS update site 50 | RewriteCond %{QUERY_STRING} ^.*version=(\d)\.(\d+)\.(\d+)(|[-].*)$ [NC] 51 | RewriteCond %1 >${major} 52 | RewriteRule ^(update\-center.*\.(json|html)+) /dynamic-stable-${major}\.${minor}\.${patch}%{REQUEST_URI}? [NC,L,R] 53 | RewriteCond %{QUERY_STRING} ^.*version=(\d)\.(\d+)\.(\d+)(|[-].*)$ [NC] 54 | RewriteCond %1 =${major} 55 | RewriteCond %2 >${minor} 56 | RewriteRule ^(update\-center.*\.(json|html)+) /dynamic-stable-${major}\.${minor}\.${patch}%{REQUEST_URI}? [NC,L,R] 57 | RewriteCond %{QUERY_STRING} ^.*version=(\d)\.(\d+)\.(\d+)(|[-].*)$ [NC] 58 | RewriteCond %1 =${major} 59 | RewriteCond %2 =${minor} 60 | RewriteCond %3 >=${patch} 61 | RewriteRule ^(update\-center.*\.(json|html)+) /dynamic-stable-${major}\.${minor}\.${patch}%{REQUEST_URI}? [NC,L,R] 62 | EOF 63 | oldestStable="$version" 64 | else 65 | # This is a weekly version 66 | # Split our version up into an array for rewriting 67 | # 1.651 becomes (1 651) 68 | oldestWeekly="$version" 69 | cat < ${major} or major = ${major} and minor >= ${minor}, use this weekly update site 72 | RewriteCond %{QUERY_STRING} ^.*version=(\d)\.(\d+)(|[-].*)$ [NC] 73 | RewriteCond %1 >${major} 74 | RewriteRule ^(update\-center.*\.(json|html)+) /dynamic-${major}\.${minor}%{REQUEST_URI}? [NC,L,R] 75 | RewriteCond %{QUERY_STRING} ^.*version=(\d)\.(\d+)(|[-].*)$ [NC] 76 | RewriteCond %1 =${major} 77 | RewriteCond %2 >=${minor} 78 | RewriteRule ^(update\-center.*\.(json|html)+) /dynamic-${major}\.${minor}%{REQUEST_URI}? [NC,L,R] 79 | EOF 80 | 81 | fi 82 | done 83 | 84 | cat < [extra update-center2.jar args ...]" >&2 ; exit 1 ; } 5 | [[ -n "$1" ]] || { echo "Non-empty www root dir required" >&2 ; exit 1 ; } 6 | [[ -n "$2" ]] || { echo "Non-empty download root dir required" >&2 ; exit 1 ; } 7 | 8 | [[ -n "$SECRET" ]] || { echo "SECRET env var not defined" >&2 ; exit 1 ; } 9 | [[ -d "$SECRET" ]] || { echo "SECRET env var not a directory" >&2 ; exit 1 ; } 10 | [[ -f "$SECRET/update-center.key" ]] || { echo "update-center.key does not exist in SECRET dir" >&2 ; exit 1 ; } 11 | [[ -f "$SECRET/update-center.cert" ]] || { echo "update-center.cert does not exist in SECRET dir" >&2 ; exit 1 ; } 12 | 13 | WWW_ROOT_DIR="$1" 14 | DOWNLOAD_ROOT_DIR="$2" 15 | shift 16 | shift 17 | EXTRA_ARGS="$*" 18 | 19 | set -o nounset 20 | set -o pipefail 21 | set -o errexit 22 | 23 | echo "Bash: $BASH_VERSION" >&2 24 | 25 | # platform specific behavior 26 | UNAME="$( uname )" 27 | if [[ $UNAME == Linux ]] ; then 28 | SORT="sort" 29 | elif [[ $UNAME == Darwin ]] ; then 30 | SORT="gsort" 31 | else 32 | echo "Unknown platform: $UNAME" >&2 33 | exit 1 34 | fi 35 | 36 | function test_which { 37 | command -v "$1" >/dev/null || { echo "Not on PATH: $1" >&2 ; exit 1 ; } 38 | } 39 | 40 | TOOLS=( curl wget "$SORT" jq ) 41 | 42 | for tool in "${TOOLS[@]}" ; do 43 | test_which "$tool" 44 | done 45 | 46 | # We have associated resource files, so determine script directory -- greadlink is GNU coreutils readlink on Mac OS with Homebrew 47 | SIMPLE_SCRIPT_DIR="$( dirname "$0" )" 48 | MAIN_DIR="$( readlink -f "$SIMPLE_SCRIPT_DIR/../" 2>/dev/null || greadlink -f "$SIMPLE_SCRIPT_DIR/../" )" || { echo "Failed to determine script directory using (g)readlink -f" >&2 ; exit 1 ; } 49 | 50 | echo "Main directory: $MAIN_DIR" 51 | mkdir -p "$MAIN_DIR"/tmp/ 52 | 53 | version=3.18.2 54 | coordinates=org/jenkins-ci/update-center2/$version/update-center2-$version-bin.zip 55 | 56 | if [[ -f "$MAIN_DIR"/tmp/generator-$version.zip ]] ; then 57 | echo "tmp/generator-$version.zip already exists, skipping download" 58 | else 59 | echo "tmp/generator-$version.zip does not exist, downloading ..." 60 | rm -rf "$MAIN_DIR"/tmp/generator*.zip 61 | wget --no-verbose -O "$MAIN_DIR"/tmp/generator-$version.zip "https://repo.jenkins-ci.org/releases/$coordinates" 62 | fi 63 | 64 | rm -rf "$MAIN_DIR"/tmp/generator/ 65 | unzip -q "$MAIN_DIR"/tmp/generator-$version.zip -d "$MAIN_DIR"/tmp/generator/ 66 | 67 | function execute { 68 | # The fastjson library cannot handle a file.encoding of US-ASCII even when manually specifying the encoding at every opportunity, so set a sane default here. 69 | # Define the default for certificate expiration and recent release age threshold, but if environment variables of the same name are defined, use their values. 70 | java -DCERTIFICATE_MINIMUM_VALID_DAYS="${CERTIFICATE_MINIMUM_VALID_DAYS:-30}" -DRECENT_RELEASES_MAX_AGE_HOURS="${RECENT_RELEASES_MAX_AGE_HOURS:-3}" -Dfile.encoding=UTF-8 -jar "$MAIN_DIR"/tmp/generator/update-center2-*.jar "$@" 71 | # To use a locally built snapshot, use the following command instead: 72 | # java -Dfile.encoding=UTF-8 -jar target/update-center2-*-bin/update-center2-*.jar "$@" 73 | } 74 | 75 | execute --dynamic-tier-list-file tmp/tiers.json 76 | readarray -t WEEKLY_RELEASES < <( jq --raw-output '.weeklyCores[]' tmp/tiers.json ) || { echo "Failed to determine weekly tier list" >&2 ; exit 1 ; } 77 | readarray -t STABLE_RELEASES < <( jq --raw-output '.stableCores[]' tmp/tiers.json ) || { echo "Failed to determine stable tier list" >&2 ; exit 1 ; } 78 | 79 | # prepare the www workspace for execution 80 | rm -rf "$WWW_ROOT_DIR" 81 | mkdir -p "$WWW_ROOT_DIR" 82 | 83 | # Generate htaccess file 84 | "$( dirname "$0" )"/generate-htaccess.sh "${WEEKLY_RELEASES[@]}" "${STABLE_RELEASES[@]}" > "$WWW_ROOT_DIR/.htaccess" 85 | 86 | # Reset arguments file 87 | echo "# one update site per line" > "$MAIN_DIR"/tmp/args.lst 88 | 89 | function generate { 90 | echo "--key $SECRET/update-center.key --certificate $SECRET/update-center.cert --root-certificate $( dirname "$0" )/../resources/certificates/jenkins-update-center-root-ca-2.crt --index-template-url https://www.jenkins.io/templates/downloads/ $EXTRA_ARGS $*" >> "$MAIN_DIR"/tmp/args.lst 91 | } 92 | 93 | function sanity-check { 94 | dir="$1" 95 | file="$dir/update-center.json" 96 | if [[ 1500000 -ge $( wc -c < "$file" ) ]] ; then 97 | echo "Sanity check: $file looks too small" >&2 98 | exit 1 99 | else 100 | echo "Sanity check: $file looks OK" >&2 101 | fi 102 | } 103 | 104 | # Generate tiered update sites for different segments so that plugins can 105 | # aggressively update baseline requirements without stranding earlier users. 106 | # 107 | # We generate tiered update sites for all core releases newer than 108 | # about 13 months that are actually used as plugin dependencies. 109 | # This supports updating Jenkins (core) once a year while getting offered compatible plugin updates. 110 | for version in "${WEEKLY_RELEASES[@]}" ; do 111 | # For mainline, advertising the latest core 112 | generate --limit-plugin-core-dependency "$version" --write-latest-core --write-timestamp --www-dir "$WWW_ROOT_DIR/dynamic-$version" 113 | done 114 | 115 | for version in "${STABLE_RELEASES[@]}" ; do 116 | # For LTS, advertising the latest LTS core 117 | generate --limit-plugin-core-dependency "$version" --write-latest-core --write-timestamp --www-dir "$WWW_ROOT_DIR/dynamic-stable-$version" --only-stable-core 118 | done 119 | 120 | # Experimental update center without version caps, including experimental releases. 121 | # This is not a part of the version-based redirection rules, admins need to manually configure it. 122 | # Generate this first, including --downloads-directory, as this includes all releases, experimental and otherwise. 123 | generate --www-dir "$WWW_ROOT_DIR/experimental" --generate-recent-releases --with-experimental --downloads-directory "$DOWNLOAD_ROOT_DIR" --latest-links-directory "$WWW_ROOT_DIR/experimental/latest" 124 | 125 | # Current update site without version caps, excluding experimental releases. 126 | # This generates -download after the experimental update site above to change the 'latest' symlinks to the latest released version. 127 | # This also generates --download-links-directory to only visibly show real releases on index.html pages. 128 | generate --generate-release-history --generate-recent-releases --generate-plugin-versions --generate-plugin-documentation-urls \ 129 | --write-latest-core --write-timestamp --write-plugin-count \ 130 | --www-dir "$WWW_ROOT_DIR/current" --download-links-directory "$WWW_ROOT_DIR/download" --downloads-directory "$DOWNLOAD_ROOT_DIR" --latest-links-directory "$WWW_ROOT_DIR/current/latest" 131 | 132 | # Actually run the update center build. 133 | execute --resources-dir "$MAIN_DIR"/resources --arguments-file "$MAIN_DIR"/tmp/args.lst 134 | 135 | # Generate symlinks to global /updates directory (created by crawler) 136 | for version in "${WEEKLY_RELEASES[@]}" ; do 137 | sanity-check "$WWW_ROOT_DIR/dynamic-$version" 138 | ln -sf ../updates "$WWW_ROOT_DIR/dynamic-$version/updates" 139 | done 140 | 141 | for version in "${STABLE_RELEASES[@]}" ; do 142 | sanity-check "$WWW_ROOT_DIR/dynamic-stable-$version" 143 | ln -sf ../updates "$WWW_ROOT_DIR/dynamic-stable-$version/updates" 144 | 145 | # needed for the stable/ directory (below) 146 | lastLTS=dynamic-stable-$version 147 | done 148 | 149 | sanity-check "$WWW_ROOT_DIR/experimental" 150 | sanity-check "$WWW_ROOT_DIR/current" 151 | ln -sf ../updates "$WWW_ROOT_DIR/experimental/updates" 152 | ln -sf ../updates "$WWW_ROOT_DIR/current/updates" 153 | 154 | 155 | # generate symlinks to retain compatibility with past layout and make Apache index useful 156 | pushd "$WWW_ROOT_DIR" 157 | ln -s "$lastLTS" stable 158 | for f in latest latestCore.txt plugin-documentation-urls.json release-history.json plugin-versions.json update-center.json update-center.actual.json update-center.json.html ; do 159 | ln -s "current/$f" . 160 | done 161 | popd 162 | 163 | # copy other static resource files 164 | echo '{}' > "$WWW_ROOT_DIR/uctest.json" 165 | wget -q --convert-links -O "$WWW_ROOT_DIR/index.html" --convert-links https://www.jenkins.io/templates/updates/index.html 166 | cp -av "tmp/tiers.json" "$WWW_ROOT_DIR/tiers.json" 167 | -------------------------------------------------------------------------------- /site/static/readme.html: -------------------------------------------------------------------------------- 1 |

2 | Important: 3 | Do not rely on the existence or layout of subdirectories shown here. 4 | To obtain versioned metadata for a specific Jenkins version, add a ?version= query parameter, for example /update-center.json?version=2.234. 5 | Jenkins does this automatically, so the update site URL configured in Jenkins should always be just https://updates.jenkins.io/update-center.json. 6 |

7 |

8 | You can rsync these files via rsync -avz rsync://rsync.osuosl.org/jenkins/updates/ somewhere. 9 |

10 |

11 | For more information about the layout of update center, 12 | see this document. 13 |

-------------------------------------------------------------------------------- /site/test/.gitignore: -------------------------------------------------------------------------------- 1 | /htaccess.tmp 2 | -------------------------------------------------------------------------------- /site/test/Dockerfile: -------------------------------------------------------------------------------- 1 | # Test image only containing the generated .htaccess file with redirects 2 | FROM httpd:2.4 3 | 4 | # It's the default file except we AllowOverride All, and enable mod_rewrite 5 | COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf 6 | COPY ./htaccess.tmp /usr/local/apache2/htdocs/.htaccess 7 | 8 | RUN echo "{}" > /usr/local/apache2/htdocs/uctest.json 9 | RUN chmod a+r /usr/local/apache2/htdocs/.htaccess 10 | -------------------------------------------------------------------------------- /spotbugs-excludes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | bin 3 | 4 | dir 5 | zip 6 | 7 | false 8 | 9 | 10 | runtime 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/ArtifactCoordinates.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import java.util.Objects; 4 | 5 | public class ArtifactCoordinates { 6 | 7 | public final String groupId; 8 | public final String artifactId; 9 | public final String version; 10 | public final String packaging; 11 | 12 | public ArtifactCoordinates(String groupId, String artifactId, String version, String packaging) { 13 | this.groupId = groupId; 14 | this.artifactId = artifactId; 15 | this.version = version; 16 | this.packaging = packaging; 17 | } 18 | 19 | public String getGav() { 20 | return groupId + ":" + artifactId + ":" + version; 21 | } 22 | 23 | public String toString() { 24 | return groupId + ":" + artifactId + ":" + version + ":" + packaging; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | ArtifactCoordinates that = (ArtifactCoordinates) o; 32 | return Objects.equals(groupId, that.groupId) && 33 | Objects.equals(artifactId, that.artifactId) && 34 | Objects.equals(version, that.version) && 35 | Objects.equals(packaging, that.packaging); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return Objects.hash(groupId, artifactId, version, packaging); 41 | } 42 | 43 | /** 44 | * @return whether the first segment of the version (delimited by . or -) 45 | * is a number. 46 | */ 47 | public boolean isVersionValid() { 48 | return version.matches("[0-9]+([-.].*)?"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/BaseMavenRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import hudson.util.VersionNumber; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.nio.file.Files; 10 | import java.util.Collection; 11 | import java.util.HashSet; 12 | import java.util.Map; 13 | import java.util.Properties; 14 | import java.util.Set; 15 | import java.util.TreeMap; 16 | import java.util.logging.Level; 17 | import java.util.stream.Stream; 18 | 19 | /** 20 | * A collection of artifacts from which we build index. 21 | */ 22 | public abstract class BaseMavenRepository implements MavenRepository { 23 | 24 | private static final Properties IGNORE = new Properties(); 25 | 26 | static { 27 | try (InputStream stream = Files.newInputStream(new File(Main.resourcesDir, 28 | "artifact-ignores.properties").toPath())) { 29 | IGNORE.load(stream); 30 | } catch (IOException e) { 31 | throw new Error(e); 32 | } 33 | } 34 | 35 | public static Stream getIgnoresWithDeprecationUrl() { 36 | return IGNORE.keySet().stream().filter(s -> isUrl(IGNORE.getProperty(s.toString()))); 37 | } 38 | 39 | private static boolean isUrl(String property) { 40 | return !StringUtils.isEmpty(property) && !property.trim().startsWith("#"); 41 | } 42 | 43 | public static String getIgnoreNoticeUrl(String artifactId) { 44 | return IGNORE.getProperty(artifactId); 45 | } 46 | 47 | public Collection listJenkinsPlugins() throws IOException { 48 | 49 | Map plugins = 50 | new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 51 | 52 | Set excluded = new HashSet<>(); 53 | final Collection results = listAllPlugins(); 54 | 55 | for (ArtifactCoordinates artifactCoordinates : results) { 56 | if (artifactCoordinates.version.contains("SNAPSHOT")) continue; // ignore snapshots 57 | if (artifactCoordinates.version.contains("JENKINS")) continue; // non-public releases for addressing specific bug fixes 58 | // Don't add suspended artifacts 59 | if (IGNORE.containsKey(artifactCoordinates.artifactId)) { 60 | if (excluded.add(artifactCoordinates.artifactId)) { 61 | LOGGER.log(Level.CONFIG, "Ignoring " + artifactCoordinates.artifactId + " because this artifact is suspended"); 62 | } 63 | continue; 64 | } 65 | if (IGNORE.containsKey(artifactCoordinates.artifactId + "@" + artifactCoordinates.version)) { 66 | LOGGER.log(Level.CONFIG, "Ignoring " + artifactCoordinates.artifactId + ", version " + artifactCoordinates.version + " because this version is suspended"); 67 | continue; 68 | } 69 | if (!artifactCoordinates.isVersionValid()) { 70 | LOGGER.log(Level.CONFIG, "Ignoring " + artifactCoordinates.artifactId + ", version " + artifactCoordinates.version + " because this version is not valid"); 71 | continue; 72 | } 73 | 74 | Plugin plugin = plugins.get(artifactCoordinates.artifactId); 75 | if (plugin == null) { 76 | plugin = new Plugin(artifactCoordinates.artifactId); 77 | plugins.put(artifactCoordinates.artifactId, plugin); 78 | } 79 | HPI hpi = new HPI(this, artifactCoordinates, plugin); 80 | 81 | plugin.addArtifact(hpi); 82 | } 83 | final TreeMap ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 84 | ret.putAll(plugins); 85 | return ret.values(); 86 | } 87 | 88 | /** 89 | * Discover all hudson.war versions. Map must be sorted by version number, descending. 90 | */ 91 | public TreeMap getJenkinsWarsByVersionNumber() throws IOException { 92 | TreeMap r = new TreeMap<>(VersionNumber.DESCENDING); 93 | addWarsInGroupIdToMap(r, "org.jenkins-ci.main", null); 94 | addWarsInGroupIdToMap(r, "org.jvnet.hudson.main", JenkinsWar.HUDSON_CUT_OFF); 95 | return r; 96 | } 97 | 98 | protected abstract Set listAllJenkinsWars(String groupId) throws IOException; 99 | 100 | public void addWarsInGroupIdToMap(Map releases, String groupId, VersionNumber cap) throws IOException { 101 | final Set results = listAllJenkinsWars(groupId); 102 | for (ArtifactCoordinates artifactCoordinates : results) { 103 | if (artifactCoordinates.version.contains("SNAPSHOT")) continue; // ignore snapshots 104 | if (artifactCoordinates.version.contains("JENKINS")) continue; // non-public releases for addressing specific bug fixes 105 | if (!artifactCoordinates.artifactId.equals("jenkins-war") 106 | && !artifactCoordinates.artifactId.equals("hudson-war")) continue; // somehow using this as a query results in 0 hits. 107 | if (IGNORE.containsKey(artifactCoordinates.artifactId + "@" + artifactCoordinates.version)) { 108 | LOGGER.log(Level.CONFIG, "Ignoring " + artifactCoordinates.artifactId + ", version " + artifactCoordinates.version + " because this version is suspended"); 109 | continue; 110 | } 111 | if (cap != null && new VersionNumber(artifactCoordinates.version).compareTo(cap) > 0) continue; 112 | 113 | VersionNumber version = new VersionNumber(artifactCoordinates.version); 114 | releases.put(version, new JenkinsWar(this, artifactCoordinates)); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/DefaultMavenRepositoryBuilder.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import io.jenkins.update_center.util.Environment; 4 | 5 | public class DefaultMavenRepositoryBuilder { 6 | 7 | private static String ARTIFACTORY_API_USERNAME = Environment.getString("ARTIFACTORY_USERNAME"); 8 | private static String ARTIFACTORY_API_PASSWORD = Environment.getString("ARTIFACTORY_PASSWORD"); 9 | 10 | private DefaultMavenRepositoryBuilder () { 11 | 12 | } 13 | 14 | private static BaseMavenRepository instance; 15 | 16 | public static synchronized BaseMavenRepository getInstance() { 17 | if (instance == null) { 18 | if (ARTIFACTORY_API_PASSWORD != null && ARTIFACTORY_API_USERNAME != null) { 19 | instance = new ArtifactoryRepositoryImpl(ARTIFACTORY_API_USERNAME, ARTIFACTORY_API_PASSWORD); 20 | } else { 21 | throw new IllegalStateException("ARTIFACTORY_USERNAME and ARTIFACTORY_PASSWORD need to be set"); 22 | } 23 | } 24 | return instance; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/Deprecations.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.nio.file.Files; 7 | import java.util.Properties; 8 | import java.util.stream.Stream; 9 | 10 | public class Deprecations { 11 | private Deprecations() {} 12 | 13 | public static String getCustomDeprecationUri(String pluginName) { 14 | return DEPRECATIONS.getProperty(pluginName); 15 | } 16 | 17 | public static Stream getDeprecatedPlugins() { 18 | return Stream.concat(DEPRECATIONS.keySet().stream(), 19 | BaseMavenRepository.getIgnoresWithDeprecationUrl()) 20 | .map(Object::toString); 21 | } 22 | 23 | private static final Properties DEPRECATIONS = new Properties(); 24 | 25 | static { 26 | try (InputStream stream = Files.newInputStream(new File(Main.resourcesDir, "deprecations.properties").toPath())){ 27 | DEPRECATIONS.load(stream); 28 | } catch (IOException e) { 29 | throw new Error(e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/HealthScores.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | import java.net.http.HttpClient; 6 | import java.net.http.HttpRequest; 7 | import java.net.http.HttpResponse; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import java.util.stream.Collectors; 13 | 14 | import com.alibaba.fastjson.JSON; 15 | 16 | /** 17 | * Health score is an integer, from 0 to 100, which represents the health of the plugin. 18 | * The health is determined by the Plugin Health Scoring project hosted on https://github.com/jenkins-infra/plugin-health-scoring. 19 | *

20 | * The list of plugins on which the scores are computed by this project is coming from the update-center file. 21 | * This means that when a plugin is for the first time in the update-center, it won't have any score. 22 | *

23 | */ 24 | public class HealthScores { 25 | private static final Logger LOGGER = Logger.getLogger(HealthScores.class.getName()); 26 | private static final String HEALTH_SCORES_URL = "https://reports.jenkins.io/plugin-health-scoring/scores.json"; 27 | 28 | private static HealthScores instance; 29 | private final Map healthScores; 30 | 31 | private HealthScores(Map healthScores) { 32 | this.healthScores = healthScores; 33 | } 34 | 35 | public static synchronized HealthScores getInstance() { 36 | if (instance == null) { 37 | initialize(); 38 | } 39 | return instance; 40 | } 41 | 42 | private static void initialize() { 43 | final HttpRequest request = HttpRequest.newBuilder() 44 | .uri(URI.create(HEALTH_SCORES_URL)) 45 | .GET() 46 | .build(); 47 | 48 | try (HttpClient client = HttpClient.newHttpClient()) { 49 | final HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); 50 | final JsonResponse jsonResponse = JSON.parseObject(response.body(), JsonResponse.class); 51 | if (jsonResponse.plugins == null) { 52 | throw new IOException("Specified healthScore URL '" + HEALTH_SCORES_URL + "' does not contain a JSON object 'plugins'"); 53 | } 54 | 55 | final Map healthScores = jsonResponse.plugins.keySet().stream() 56 | .collect(Collectors.toMap(Function.identity(), pluginId -> jsonResponse.plugins.get(pluginId).value)); 57 | instance = new HealthScores(healthScores); 58 | } catch (IOException | InterruptedException e) { 59 | LOGGER.log(Level.WARNING, e.getMessage()); 60 | instance = new HealthScores(Map.of()); 61 | } 62 | } 63 | 64 | private static class JsonResponse { 65 | public Map plugins; 66 | 67 | } 68 | private static class PluginResponse { 69 | public int value; 70 | } 71 | 72 | public Integer getHealthScore(String pluginId) { 73 | return this.healthScores.get(pluginId); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/IndexHtmlBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2004-2020, Sun Microsystems, Inc. and other contributors 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center; 25 | 26 | import org.apache.commons.codec.binary.Hex; 27 | import org.apache.commons.io.output.NullWriter; 28 | import org.bouncycastle.util.encoders.Base64; 29 | 30 | import java.io.Closeable; 31 | import java.io.File; 32 | import java.io.FileOutputStream; 33 | import java.io.IOException; 34 | import java.io.OutputStreamWriter; 35 | import java.io.PrintWriter; 36 | import java.nio.charset.StandardCharsets; 37 | import java.text.SimpleDateFormat; 38 | import java.util.Date; 39 | 40 | /** 41 | * Generates index.html that has a list of files. 42 | * 43 | * @author Kohsuke Kawaguchi 44 | */ 45 | public class IndexHtmlBuilder implements Closeable { 46 | private final PrintWriter out; 47 | private final String template; 48 | private final String title; 49 | private String subtitle; 50 | private final String description; 51 | private final StringBuilder content; 52 | private final String opengraphImage; 53 | 54 | public IndexHtmlBuilder(File dir, String title, String globalTemplate) throws IOException { 55 | this.out = openIndexHtml(dir); 56 | this.template = globalTemplate; 57 | this.title = title; 58 | this.content = new StringBuilder(); 59 | this.subtitle = ""; 60 | this.description = "Download previous versions of " + title; 61 | this.opengraphImage = "https://www.jenkins.io/images/logo-title-opengraph.png"; 62 | } 63 | 64 | public IndexHtmlBuilder withSubtitle(String subtitle) { 65 | if (subtitle != null) { 66 | this.subtitle = subtitle; 67 | } 68 | return this; 69 | } 70 | 71 | private static PrintWriter openIndexHtml(File dir) throws IOException { 72 | if (dir == null) { 73 | return new PrintWriter(new NullWriter()); // ignore output 74 | } 75 | 76 | if (!dir.mkdirs() && !dir.isDirectory()) { 77 | throw new IllegalStateException("Failed to create " + dir); 78 | } 79 | return new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(dir, "index.html")), StandardCharsets.UTF_8)); 80 | } 81 | 82 | private String base64ToHex(String base64) { 83 | byte[] decodedBase64 = Base64.decode(base64.getBytes(StandardCharsets.US_ASCII)); 84 | return Hex.encodeHexString(decodedBase64); 85 | } 86 | 87 | public void add(MavenArtifact a) throws IOException { 88 | MavenRepository.ArtifactMetadata artifactMetadata = a.getMetadata(); 89 | if (artifactMetadata == null) { 90 | return; 91 | } 92 | if (a instanceof HPI) { 93 | add(a.getDownloadUrl().toExternalForm(), a.getTimestampAsDate(), a.version, artifactMetadata, ((HPI) a).getRequiredJenkinsVersion()); 94 | } else { 95 | add(a.getDownloadUrl().toExternalForm(), a.getTimestampAsDate(), a.version, artifactMetadata, null); 96 | } 97 | } 98 | 99 | public void add(String url, String caption) { 100 | add(url, null, caption, null, null); 101 | } 102 | 103 | public void add(String url, Date releaseDate, String caption, MavenRepository.ArtifactMetadata metadata, String requiredJenkinsVersion) { 104 | String releaseDateString = ""; 105 | if (releaseDate != null) { 106 | releaseDateString = " Released: " + SimpleDateFormat.getDateInstance().format(releaseDate); 107 | } 108 | 109 | content.append("").append(caption).append("
\n
") 112 | .append(releaseDateString) 113 | .append("
"); 114 | if (metadata != null) { 115 | content.append("\n
SHA-1: ") 116 | .append(base64ToHex(metadata.sha1)).append("
"); 117 | if (metadata.sha256 != null) { 118 | content.append("\n
SHA-256: ") 119 | .append(base64ToHex(metadata.sha256)).append("
"); 120 | } 121 | } 122 | if (requiredJenkinsVersion != null) { 123 | content.append("\n
Requires Jenkins ").append(requiredJenkinsVersion).append("
"); 124 | } 125 | content.append("
\n"); 126 | } 127 | 128 | @Override 129 | public void close() { 130 | out.println(template 131 | .replace("{{ title }}", title) 132 | .replace("{{ subtitle }}", subtitle) 133 | .replace("{{ description }}", description) 134 | .replace("{{ opengraphImage }}", opengraphImage) 135 | .replace("{{ content }}", content.toString())); 136 | out.close(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/IndexTemplateProvider.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.charset.StandardCharsets; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | 10 | public class IndexTemplateProvider { 11 | private static String globalTemplate; 12 | 13 | public IndexHtmlBuilder newIndexHtmlBuilder(File dir, String title) throws IOException { 14 | if (globalTemplate == null) { 15 | globalTemplate = initTemplate(); 16 | } 17 | return new IndexHtmlBuilder(dir, title, globalTemplate); 18 | } 19 | 20 | protected String initTemplate() { 21 | Path template = Paths.get(Main.resourcesDir.getAbsolutePath(), "index-template.html"); 22 | try { 23 | return new String(Files.readAllBytes(template), StandardCharsets.UTF_8); 24 | } catch (IOException ioe) { 25 | throw new IllegalStateException(ioe); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/IssueTrackerSource.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.TypeReference; 5 | import com.alibaba.fastjson.annotation.JSONField; 6 | import io.jenkins.update_center.util.Environment; 7 | import org.apache.commons.io.IOUtils; 8 | 9 | import java.io.IOException; 10 | import java.net.URL; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.logging.Level; 15 | import java.util.logging.Logger; 16 | 17 | public class IssueTrackerSource { 18 | private static final Logger LOGGER = Logger.getLogger(IssueTrackerSource.class.getName()); 19 | 20 | private static final String DATA_URL = Environment.getString("ISSUE_TRACKER_JSON_URL", "https://reports.jenkins.io/issues.index.json"); 21 | 22 | private HashMap> pluginToIssueTrackers; 23 | 24 | public static class IssueTracker { 25 | @JSONField 26 | public String type; 27 | @JSONField 28 | public String viewUrl; 29 | @JSONField 30 | public String reportUrl; 31 | } 32 | 33 | private static IssueTrackerSource instance; 34 | 35 | public static synchronized IssueTrackerSource getInstance() { 36 | if (instance == null) { 37 | IssueTrackerSource its = new IssueTrackerSource(); 38 | its.init(); 39 | instance = its; 40 | } 41 | return instance; 42 | } 43 | 44 | private void init() { 45 | try { 46 | final String jsonData = IOUtils.toString(new URL(DATA_URL), StandardCharsets.UTF_8); 47 | pluginToIssueTrackers = JSON.parseObject(jsonData, new TypeReferenceForHashMapFromStringToListOfIssueTracker().getType()); 48 | } catch (RuntimeException | IOException ex) { 49 | LOGGER.log(Level.WARNING, ex.getMessage()); 50 | pluginToIssueTrackers = new HashMap<>(); 51 | } 52 | } 53 | 54 | public List getIssueTrackers(String plugin) { 55 | return pluginToIssueTrackers.computeIfAbsent(plugin, p -> null); // Don't advertise empty lists of issue trackers if there are none. 56 | } 57 | 58 | private static class TypeReferenceForHashMapFromStringToListOfIssueTracker extends TypeReference>> { 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/JenkinsIndexTemplateProvider.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import org.jsoup.Jsoup; 4 | import org.jsoup.nodes.Document; 5 | import org.jsoup.nodes.Element; 6 | 7 | import java.io.IOException; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | public class JenkinsIndexTemplateProvider extends IndexTemplateProvider { 12 | private final String url; 13 | 14 | public JenkinsIndexTemplateProvider(String url) { 15 | super(); 16 | this.url = url; 17 | } 18 | 19 | @Override 20 | protected String initTemplate() { 21 | String globalTemplate = ""; 22 | 23 | try { 24 | Document doc = Jsoup.connect(url).get(); 25 | 26 | doc.getElementsByAttribute("href").forEach(element -> setAbsoluteUrl(element, "href")); 27 | doc.getElementsByAttribute("src").forEach(element -> setAbsoluteUrl(element, "src")); 28 | globalTemplate = doc.toString(); 29 | } catch (IOException ioe) { 30 | LOGGER.log(Level.SEVERE, "Problem loading template", ioe); 31 | } 32 | return globalTemplate; 33 | } 34 | 35 | private void setAbsoluteUrl(Element element, String attributeName) { 36 | final String attribute = element.attr(attributeName); 37 | if (attribute.startsWith("/")) { 38 | element.attr(attributeName, "https://www.jenkins.io" + attribute); 39 | } 40 | } 41 | 42 | private static final Logger LOGGER = Logger.getLogger(JenkinsIndexTemplateProvider.class.getName()); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/JenkinsWar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2004-2020, Sun Microsystems, Inc. and other contributors 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center; 25 | 26 | import hudson.util.VersionNumber; 27 | import io.jenkins.update_center.util.Environment; 28 | import org.apache.commons.lang.StringUtils; 29 | 30 | import java.net.URL; 31 | import java.net.MalformedURLException; 32 | 33 | /** 34 | * @author Kohsuke Kawaguchi 35 | */ 36 | public class JenkinsWar extends MavenArtifact { 37 | public JenkinsWar(BaseMavenRepository repository, ArtifactCoordinates artifact) { 38 | super(repository, artifact); 39 | } 40 | 41 | @Override 42 | public URL getDownloadUrl() throws MalformedURLException { 43 | return new URL(StringUtils.removeEnd(DOWNLOADS_ROOT_URL, "/") + "/war/" + version + "/" + getFileName()); 44 | } 45 | 46 | public String getFileName() { 47 | String fileName; 48 | if (new VersionNumber(version).compareTo(HUDSON_CUT_OFF)<=0) { 49 | fileName = "hudson.war"; 50 | } else { 51 | fileName = "jenkins.war"; 52 | } 53 | return fileName; 54 | } 55 | 56 | /** 57 | * Hudson to Jenkins cut-over version. 58 | */ 59 | public static final VersionNumber HUDSON_CUT_OFF = new VersionNumber("1.395"); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/LatestLinkBuilder.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.OutputStreamWriter; 7 | import java.io.PrintWriter; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.logging.Level; 10 | import java.util.logging.Logger; 11 | 12 | /** 13 | * Generates latest/index.html and latest/.htaccess 14 | * 15 | * The former lists all the available symlinks, and the latter actually defines the redirects. 16 | * 17 | */ 18 | public class LatestLinkBuilder implements AutoCloseable { 19 | private static final Logger LOGGER = Logger.getLogger(LatestLinkBuilder.class.getName()); 20 | 21 | private final IndexHtmlBuilder index; 22 | private final PrintWriter htaccess; 23 | 24 | public LatestLinkBuilder(File dir, IndexTemplateProvider service) throws IOException { 25 | LOGGER.log(Level.FINE, String.format("Writing plugin symlinks and redirects to dir: %s", dir)); 26 | 27 | index = service.newIndexHtmlBuilder(dir,"Permalinks to latest files"); 28 | htaccess = new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(dir,".htaccess"), true), StandardCharsets.UTF_8)); 29 | 30 | htaccess.println("# GENERATED. DO NOT MODIFY."); 31 | // Redirect directive doesn't let us write redirect rules relative to the directory .htaccess exists, 32 | // so we are back to mod_rewrite 33 | htaccess.println("RewriteEngine on"); 34 | } 35 | 36 | public void close() { 37 | index.close(); 38 | htaccess.close(); 39 | } 40 | 41 | public void add(String localPath, String target) throws IOException { 42 | htaccess.printf("RewriteRule ^%s$ %s [R=302,L]%n", localPath.replace(".", "\\."), target); 43 | index.add(localPath, localPath); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/LatestPluginVersions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2020, Daniel Beck 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center; 25 | 26 | import hudson.util.VersionNumber; 27 | 28 | import javax.annotation.Nonnull; 29 | import java.io.IOException; 30 | import java.util.Collections; 31 | import java.util.Map; 32 | import java.util.Objects; 33 | import java.util.stream.Collectors; 34 | 35 | /** 36 | * Utility class for including the latest published version of a plugin in update site metadata. 37 | * 38 | *

Limitations in update site tiering deliberately result in older releases being offered, and this additional 39 | * metadata allows informing users that the update site does not offer the very latest releases.

40 | * 41 | *

Despite the name, unrelated to {@code latest/} directories created by {@link LatestLinkBuilder}.

42 | */ 43 | public class LatestPluginVersions { 44 | private static LatestPluginVersions instance; 45 | 46 | private final Map latestVersions; 47 | 48 | private LatestPluginVersions(@Nonnull MavenRepository repository) throws IOException { 49 | this.latestVersions = repository.listJenkinsPlugins().stream().collect(Collectors.toMap(Plugin::getArtifactId, p -> p.getLatest().getVersion())); 50 | } 51 | 52 | private LatestPluginVersions(@Nonnull Map latestVersions) { 53 | this.latestVersions = latestVersions; 54 | } 55 | 56 | public static void initialize(@Nonnull MavenRepository repository) throws IOException { 57 | instance = new LatestPluginVersions(repository); 58 | } 59 | 60 | public static void initializeEmpty() { 61 | instance = new LatestPluginVersions(Collections.emptyMap()); 62 | } 63 | 64 | @Nonnull 65 | public static LatestPluginVersions getInstance() { 66 | Objects.requireNonNull(instance, "instance"); 67 | return instance; 68 | } 69 | 70 | public VersionNumber getLatestVersion(String pluginId) { 71 | return latestVersions.get(pluginId); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/MaintainersSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2021, Daniel Beck 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center; 25 | 26 | import com.alibaba.fastjson.JSON; 27 | import com.alibaba.fastjson.TypeReference; 28 | import com.alibaba.fastjson.annotation.JSONField; 29 | import io.jenkins.update_center.util.Environment; 30 | import org.apache.commons.io.IOUtils; 31 | 32 | import java.io.IOException; 33 | import java.net.URL; 34 | import java.nio.charset.StandardCharsets; 35 | import java.util.AbstractMap; 36 | import java.util.ArrayList; 37 | import java.util.HashMap; 38 | import java.util.List; 39 | import java.util.Map; 40 | import java.util.logging.Level; 41 | import java.util.logging.Logger; 42 | import java.util.stream.Collectors; 43 | 44 | public class MaintainersSource { 45 | private static final Logger LOGGER = Logger.getLogger(MaintainersSource.class.getName()); 46 | 47 | private static final String PLUGIN_MAINTAINERS_DATA_URL = Environment.getString("PLUGIN_MAINTAINERS_DATA_URL", "https://reports.jenkins.io/maintainers.index.json"); 48 | private static final String MAINTAINERS_INFO_URL = Environment.getString("MAINTAINERS_INFO_URL", "https://reports.jenkins.io/maintainers-info-report.json"); 49 | 50 | private Map> pluginToMaintainers; 51 | private Map maintainerInfo; 52 | 53 | /** 54 | * Utility class for parsing JSON from {@link #MAINTAINERS_INFO_URL}. 55 | */ 56 | private static class JsonMaintainer { 57 | @JSONField 58 | public String displayName; 59 | 60 | @JSONField 61 | public String name; 62 | 63 | private Maintainer toMaintainer() { 64 | return new Maintainer(name, displayName); 65 | } 66 | } 67 | 68 | public static class Maintainer { 69 | private final String name; 70 | private final String developerId; 71 | 72 | public Maintainer(String id, String name) { 73 | this.developerId = id; 74 | this.name = name; 75 | } 76 | 77 | public String getDeveloperId() { 78 | return developerId; 79 | } 80 | 81 | public String getName() { 82 | return name; 83 | } 84 | } 85 | 86 | private static MaintainersSource instance; 87 | 88 | public static synchronized MaintainersSource getInstance() { 89 | if (instance == null) { 90 | MaintainersSource ms = new MaintainersSource(); 91 | ms.init(); 92 | instance = ms; 93 | } 94 | return instance; 95 | } 96 | 97 | private void init() { 98 | // Obtain maintainer info 99 | try { 100 | final String jsonData = IOUtils.toString(new URL(MAINTAINERS_INFO_URL), StandardCharsets.UTF_8); 101 | final List rawMaintainersInfo = JSON.parseObject(jsonData, new TypeReferenceForListOfJsonMaintainer().getType()); 102 | maintainerInfo = new HashMap<>(); 103 | rawMaintainersInfo.forEach(m -> { 104 | if (maintainerInfo.containsKey(m.name)) { 105 | LOGGER.warning("Duplicate entry for " + m.name + " in " + MAINTAINERS_INFO_URL); 106 | } 107 | maintainerInfo.put(m.name, m.toMaintainer()); 108 | }); 109 | } catch (RuntimeException | IOException ex) { 110 | LOGGER.log(Level.WARNING, "Failed to process " + MAINTAINERS_INFO_URL, ex); 111 | maintainerInfo = new HashMap<>(); 112 | } 113 | 114 | // Obtain plugin/maintainers mapping 115 | try { 116 | final String jsonData = IOUtils.toString(new URL(PLUGIN_MAINTAINERS_DATA_URL), StandardCharsets.UTF_8); 117 | pluginToMaintainers = JSON.parseObject(jsonData, new TypeReferenceForHashMapFromStringToListOfString().getType()); 118 | } catch (RuntimeException | IOException ex) { 119 | pluginToMaintainers = new HashMap<>(); 120 | LOGGER.log(Level.WARNING, "Failed to process" + PLUGIN_MAINTAINERS_DATA_URL, ex); 121 | } 122 | } 123 | 124 | private List getMaintainerIDs(ArtifactCoordinates plugin) { 125 | final String ga = plugin.groupId + ":" + plugin.artifactId; 126 | if (pluginToMaintainers.containsKey(ga)) { 127 | return pluginToMaintainers.get(ga); 128 | } 129 | final List candidateGAs = pluginToMaintainers.keySet().stream().filter(s -> s.endsWith(":" + plugin.artifactId)).collect(Collectors.toList()); 130 | switch (candidateGAs.size()) { 131 | case 1: 132 | final String key = candidateGAs.get(0); 133 | LOGGER.log(Level.INFO, "Apparent mismatch of group IDs between permissions assignment: " + key + " and latest available release of plugin: " + plugin); 134 | return pluginToMaintainers.get(key); 135 | case 0: 136 | // No maintainer information found 137 | LOGGER.log(Level.INFO, "No maintainer information found for plugin: " + plugin); 138 | return new ArrayList<>(); 139 | default: // 2+ candidate artifacts but none match exactly 140 | LOGGER.log(Level.WARNING, "Multiple artifact IDs match, but none exactly. Will not provide maintainer information for plugin: " + plugin); 141 | return new ArrayList<>(); 142 | } 143 | } 144 | 145 | /** 146 | * Return the list of maintainers for the specified plugin. 147 | * 148 | * @param plugin the plugin. 149 | * @return list of maintainers 150 | */ 151 | public List getMaintainers(ArtifactCoordinates plugin) { 152 | return getMaintainerIDs(plugin).stream().map((String key) -> maintainerInfo.getOrDefault(key, new Maintainer(key, null))).collect(Collectors.toList()); 153 | } 154 | 155 | private static class TypeReferenceForListOfJsonMaintainer extends TypeReference> { 156 | } 157 | private static class TypeReferenceForHashMapFromStringToListOfString extends TypeReference>> { 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/MavenArtifact.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2004-2020, Sun Microsystems, Inc. and other contributors 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center; 25 | 26 | import hudson.util.VersionNumber; 27 | import io.jenkins.update_center.util.Environment; 28 | 29 | import javax.annotation.Nonnull; 30 | import java.io.File; 31 | import java.io.IOException; 32 | import java.net.MalformedURLException; 33 | import java.net.URL; 34 | import java.text.SimpleDateFormat; 35 | import java.util.Calendar; 36 | import java.util.Date; 37 | import java.util.GregorianCalendar; 38 | import java.util.Locale; 39 | import java.util.jar.Attributes; 40 | import java.util.jar.Manifest; 41 | 42 | /** 43 | * Artifact from a Maven repository and its metadata. 44 | * 45 | * @author Kohsuke Kawaguchi 46 | */ 47 | public class MavenArtifact { 48 | protected static final String DOWNLOADS_ROOT_URL = Environment.getString("DOWNLOADS_ROOT_URL", "https://updates.jenkins.io/download"); 49 | /** 50 | * Where did this plugin come from? 51 | */ 52 | public final BaseMavenRepository repository; 53 | public final ArtifactCoordinates artifact; 54 | public final String version; 55 | private File hpi; 56 | 57 | private Manifest manifest; 58 | 59 | public MavenArtifact(@Nonnull BaseMavenRepository repository, @Nonnull ArtifactCoordinates artifact) { 60 | this.artifact = artifact; 61 | this.repository = repository; 62 | version = artifact.version; 63 | } 64 | 65 | public File resolve() throws IOException { 66 | try { 67 | if (hpi == null) { 68 | hpi = repository.resolve(artifact); 69 | } 70 | return hpi; 71 | } catch (IllegalArgumentException e) { 72 | throw new IOException("Failed to resolve artifact " + artifact, e); 73 | } 74 | } 75 | 76 | public File resolvePOM() throws IOException { 77 | return repository.resolve(artifact,"pom", null); 78 | } 79 | 80 | public MavenRepository.ArtifactMetadata getMetadata() throws IOException { 81 | return repository.getMetadata(this); 82 | } 83 | 84 | public VersionNumber getVersion() { 85 | return new VersionNumber(version); 86 | } 87 | 88 | public boolean isAlphaOrBeta() { 89 | String s = version.toLowerCase(Locale.ENGLISH); 90 | return s.contains("alpha") || s.contains("beta"); 91 | } 92 | 93 | public String getTimestampAsString() throws IOException { 94 | long lastModified = getTimestamp(); 95 | SimpleDateFormat bdf = getDateFormat(); 96 | 97 | return bdf.format(lastModified); 98 | } 99 | 100 | public Date getTimestampAsDate() throws IOException { 101 | long lastModified = getTimestamp(); 102 | 103 | 104 | Date lastModifiedDate = new Date(lastModified); 105 | Calendar cal = new GregorianCalendar(); 106 | cal.setTime(lastModifiedDate); 107 | cal.set(Calendar.HOUR_OF_DAY, 0); 108 | cal.set(Calendar.MINUTE, 0); 109 | cal.set(Calendar.SECOND, 0); 110 | cal.set(Calendar.MILLISECOND, 0); 111 | return cal.getTime(); 112 | } 113 | 114 | public static SimpleDateFormat getDateFormat() { 115 | return new SimpleDateFormat("MMM dd, yyyy", Locale.US); 116 | } 117 | 118 | public long getTimestamp() throws IOException { 119 | return repository.getMetadata(this).timestamp; 120 | } 121 | 122 | public Manifest getManifest() throws IOException { 123 | if (manifest==null) { 124 | manifest = repository.getManifest(this); 125 | } 126 | return manifest; 127 | } 128 | 129 | public Attributes getManifestAttributes() throws IOException { 130 | return getManifest().getMainAttributes(); 131 | } 132 | 133 | /** 134 | * Where to download from? 135 | * 136 | * @return the URL that users whould be able to download from 137 | * @throws MalformedURLException if the resulting URL is invalid 138 | */ 139 | public URL getDownloadUrl() throws MalformedURLException { 140 | return new URL("repo.jenkins-ci.org/public/"+artifact.groupId.replace('.','/')+"/"+artifact.artifactId+"/"+artifact.version+"/"+artifact.artifactId+"-"+artifact.version+"."+artifact.packaging); 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | return artifact.toString(); // TODO this is actually useless 146 | } 147 | 148 | public String getGavId() { 149 | return artifact.groupId+':'+artifact.artifactId+':'+artifact.version; 150 | } 151 | 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/MavenRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import hudson.util.VersionNumber; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Collection; 9 | import java.util.Date; 10 | import java.util.Map; 11 | import java.util.TreeMap; 12 | import java.util.jar.Manifest; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | 16 | public interface MavenRepository { 17 | Logger LOGGER = Logger.getLogger(MavenRepository.class.getName()); 18 | 19 | Collection listJenkinsPlugins() throws IOException; 20 | 21 | /** 22 | * Discover all jenkins.war / hudson.war versions. Map must be sorted by version number, descending. 23 | * 24 | * @return a map from version number to war 25 | * @throws IOException when an exception contacting the artifacts repository occurs 26 | */ 27 | TreeMap getJenkinsWarsByVersionNumber() throws IOException; 28 | 29 | void addWarsInGroupIdToMap(Map r, String groupId, VersionNumber cap) throws IOException; 30 | 31 | Collection listAllPlugins() throws IOException; 32 | 33 | ArtifactMetadata getMetadata(MavenArtifact artifact) throws IOException; 34 | 35 | Manifest getManifest(MavenArtifact artifact) throws IOException; 36 | 37 | InputStream getZipFileEntry(MavenArtifact artifact, String path) throws IOException; 38 | 39 | File resolve(ArtifactCoordinates artifact) throws IOException; 40 | 41 | default File resolve(ArtifactCoordinates a, String packaging, String classifier) throws IOException { 42 | return resolve(new ArtifactCoordinates(a.groupId, a.artifactId, a.version, packaging)); 43 | } 44 | 45 | /** 46 | * Discover all plugins from this Maven repository in order released, not using PluginHistory. 47 | * Only the latest release for a given release on a given day will be included. 48 | * 49 | * @return Nested maps, mapping release date (day only, at midnight), then plugin ID to plugin release. 50 | * 51 | * @throws IOException when an exception contacting the artifacts repository occurs 52 | */ 53 | default Map> listPluginsByReleaseDate() throws IOException { 54 | Collection all = listJenkinsPlugins(); 55 | 56 | Map> plugins = new TreeMap<>(); 57 | // TODO this is weird, we only include one release per plugin and day, and it's random which one if there are multiple 58 | 59 | for (Plugin plugin : all) { 60 | for (HPI hpi : plugin.getArtifacts().values()) { 61 | Date releaseDate = hpi.getTimestampAsDate(); 62 | LOGGER.log(Level.FINE, "adding " + hpi.artifact.artifactId + ":" + hpi.version); 63 | Map pluginsOnDate = plugins.computeIfAbsent(releaseDate, k -> new TreeMap<>()); 64 | pluginsOnDate.put(plugin.getArtifactId(),hpi); 65 | } 66 | } 67 | 68 | return plugins; 69 | } 70 | 71 | class ArtifactMetadata { 72 | public String sha1; 73 | public String sha256; 74 | 75 | /** 76 | * Epoch seconds (Unix timestamp) 77 | * 78 | */ 79 | public long timestamp; 80 | /** 81 | * File size in bytes 82 | * 83 | */ 84 | public long size; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/MetadataWriter.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import hudson.util.VersionNumber; 4 | import io.jenkins.update_center.util.Timestamp; 5 | import org.apache.commons.io.IOUtils; 6 | import org.kohsuke.args4j.Option; 7 | 8 | import javax.annotation.CheckForNull; 9 | import javax.annotation.Nonnull; 10 | import java.io.File; 11 | import java.io.FileOutputStream; 12 | import java.io.IOException; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.Objects; 15 | import java.util.TreeMap; 16 | import java.util.logging.Level; 17 | import java.util.logging.Logger; 18 | 19 | public class MetadataWriter { 20 | private static final Logger LOGGER = Logger.getLogger(MetadataWriter.class.getName()); 21 | private static final String LATEST_CORE_FILENAME = "latestCore.txt"; 22 | private static final String PLUGIN_COUNT_FILENAME = "pluginCount.txt"; 23 | private static final String TIMESTAMP_FILENAME = "timestamp.txt"; 24 | 25 | @Option(name = "--write-plugin-count", usage = "Report the number of plugins published by the update site") 26 | public boolean generatePluginCount; 27 | 28 | @Option(name = "--write-latest-core", usage = "Generate a text file with the core version offered by this update site") 29 | public boolean generateLatestCore; 30 | 31 | @Option(name = "--write-timestamp", usage = "Generate a text file with the generation timestamp of this update site") 32 | public boolean generateTimestamp; 33 | 34 | public void writeMetadataFiles(@Nonnull MavenRepository repository, @CheckForNull File outputDirectory) throws IOException { 35 | Objects.requireNonNull(repository, "repository"); 36 | 37 | if (!generateLatestCore && !generatePluginCount && !generateTimestamp) { 38 | LOGGER.log(Level.INFO, "Skipping generation of metadata files"); 39 | return; 40 | } 41 | 42 | if (outputDirectory == null) { 43 | throw new IOException("No output directory specified but generation of metadata files requested"); 44 | } 45 | 46 | if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) { 47 | throw new IOException("Failed to create " + outputDirectory); 48 | } 49 | 50 | if (generateLatestCore) { 51 | final TreeMap wars = repository.getJenkinsWarsByVersionNumber(); 52 | if (wars.isEmpty()) { 53 | LOGGER.log(Level.WARNING, () -> "Cannot write " + LATEST_CORE_FILENAME + " because there are no core versions in this update site"); 54 | } else { 55 | try (final FileOutputStream output = new FileOutputStream(new File(outputDirectory, LATEST_CORE_FILENAME))) { 56 | IOUtils.write(wars.firstKey().toString(), output, StandardCharsets.UTF_8); 57 | } 58 | } 59 | } 60 | 61 | if (generatePluginCount) { 62 | try (final FileOutputStream output = new FileOutputStream(new File(outputDirectory, PLUGIN_COUNT_FILENAME))) { 63 | IOUtils.write(Integer.toString(repository.listJenkinsPlugins().size()), output, StandardCharsets.UTF_8); 64 | } 65 | } 66 | 67 | if (generateTimestamp) { 68 | try (final FileOutputStream output = new FileOutputStream(new File(outputDirectory, TIMESTAMP_FILENAME))) { 69 | IOUtils.write(Timestamp.TIMESTAMP, output, StandardCharsets.UTF_8); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/Plugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2004-2020, Sun Microsystems, Inc. and other contributors 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center; 25 | 26 | import hudson.util.VersionNumber; 27 | 28 | import java.io.IOException; 29 | import java.util.Set; 30 | import java.util.TreeMap; 31 | import java.util.TreeSet; 32 | import java.util.logging.Level; 33 | import java.util.logging.Logger; 34 | 35 | /** 36 | * Information about a Jenkins plugin and its release history, discovered from Maven repository. 37 | * 38 | * This includes 'canonical' information about a plugin such as its URL or labels that is version independent. 39 | * TODO the above is aspirational 40 | */ 41 | public final class Plugin { 42 | private static final Logger LOGGER = Logger.getLogger(Plugin.class.getName()); 43 | private final String artifactId; 44 | 45 | private final TreeMap artifacts = new TreeMap<>(VersionNumber.DESCENDING); 46 | 47 | private final Set duplicateVersions = new TreeSet<>(); 48 | 49 | public Plugin(String shortName) { 50 | this.artifactId = shortName; 51 | } 52 | 53 | public HPI getLatest() { 54 | return artifacts.get(artifacts.firstKey()); 55 | } 56 | 57 | public HPI getFirst() { 58 | return artifacts.get(artifacts.lastKey()); 59 | } 60 | 61 | /** 62 | * Adding a plugin release carefully. 63 | * 64 | *

65 | * If another release exists with an equivalent version number (1.0 vs. 1.0.0), remove both from distribution due to nondeterminism. 66 | *

67 | * 68 | * @param hpi the plugin HPI 69 | */ 70 | public void addArtifact(HPI hpi) throws IOException { 71 | VersionNumber v; 72 | try { 73 | v = new VersionNumber(hpi.version); 74 | } catch (NumberFormatException e) { 75 | LOGGER.log(Level.WARNING, "Failed to parse version number " + hpi.version + " for " + hpi); 76 | return; 77 | } 78 | 79 | if (duplicateVersions.contains(v)) { 80 | // we've previous recorded two artifacts with the same name/version and neither has a timestamp 81 | if (hpi.getTimestamp() > 0) { 82 | // This artifact has a timestamp, add it 83 | artifacts.put(v, hpi); 84 | return; 85 | } else { 86 | LOGGER.log(Level.INFO, "Found another duplicate artifact " + hpi.artifact.getGav() + " considered identical due to non-determinism. Neither has a timestamp. Neither will be published."); 87 | // This is the third or more ambiguous version for this 88 | return; 89 | } 90 | } 91 | 92 | HPI existing = artifacts.get(v); 93 | 94 | if (existing == null) { 95 | artifacts.put(v, hpi); 96 | return; 97 | } 98 | 99 | /* 100 | * Deduplication rules: 101 | * - Prefer artifacts with timestamp over artifacts without timestamp. 102 | * - Prefer artifacts with earlier timestamp over artifacts with later timestamp. 103 | */ 104 | 105 | if (existing.getTimestamp() > 0) { 106 | if (hpi.getTimestamp() > 0) { 107 | // both have a timestamp 108 | if (existing.getTimestamp() > hpi.getTimestamp()) { 109 | // the previous recorded artifact is more recent than the proposed one 110 | LOGGER.log(Level.INFO, "The proposed artifact: " + hpi.artifact.getGav() + " is older than the existing artifact " + existing.artifact.getGav() + ", so replace it."); 111 | artifacts.put(v, hpi); 112 | } else { 113 | // the proposed artifact is the same age or newer than the previous recorded artifact, so ignore it 114 | LOGGER.log(Level.INFO, "The proposed artifact: " + hpi.artifact.getGav() + " is not older than the existing artifact " + existing.artifact.getGav() + ", so ignore it."); 115 | // no-op 116 | } 117 | } else { 118 | // the existing one has a timestamp, the proposed one does not 119 | LOGGER.log(Level.INFO, "The proposed artifact: " + hpi.artifact.getGav() + " has no timestamp (but the existing artifact " + hpi.artifact.getGav() + " does), so ignore it."); 120 | // no-op 121 | } 122 | } else { 123 | // we've previously recorded an artifact without timestamp 124 | if (hpi.getTimestamp() > 0) { 125 | // the proposed artifact has a timestamp, prefer that 126 | LOGGER.log(Level.INFO, "The proposed artifact: " + hpi.artifact.getGav() + " has a timestamp and the existing artifact " + existing.artifact.getGav() + " does not, so replace it."); 127 | artifacts.put(v, hpi); 128 | } else { 129 | // neither has a timestamp, so remove both 130 | LOGGER.log(Level.INFO, "Found a duplicate artifact " + hpi.artifact.getGav() + " (proposed) considered identical to " + existing.artifact.getGav() + " (existing) due to non-determinism. Neither has a timestamp. Neither will be published."); 131 | artifacts.remove(v); 132 | duplicateVersions.add(v); 133 | } 134 | } 135 | } 136 | 137 | /** 138 | * ArtifactID equals short name. 139 | * 140 | * @return the artifact ID (short name) 141 | */ 142 | public String getArtifactId() { 143 | return artifactId; 144 | } 145 | 146 | /** 147 | * All discovered versions, by the version numbers, newer versions first. 148 | * 149 | * @return a map from version number to HPI 150 | */ 151 | public TreeMap getArtifacts() { 152 | return artifacts; 153 | } 154 | 155 | @Override 156 | public String toString() { 157 | return "Plugin '" + artifactId + "'"; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/PluginFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2018 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center; 25 | 26 | import javax.annotation.CheckReturnValue; 27 | import javax.annotation.Nonnull; 28 | 29 | /** 30 | * Filters plugins in {@link BaseMavenRepository}. 31 | * 32 | * @see BaseMavenRepository 33 | */ 34 | public interface PluginFilter { 35 | 36 | @CheckReturnValue 37 | boolean shouldIgnore(@Nonnull HPI hpi); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/PluginUpdateCenterEntry.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import hudson.util.VersionNumber; 5 | 6 | import java.util.ArrayList; 7 | import javax.annotation.CheckForNull; 8 | import java.io.IOException; 9 | import java.net.MalformedURLException; 10 | import java.net.URL; 11 | import java.text.SimpleDateFormat; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.Locale; 15 | import java.util.logging.Level; 16 | import java.util.logging.Logger; 17 | 18 | /** 19 | * An entry of a plugin in the update center metadata. 20 | * 21 | */ 22 | public class PluginUpdateCenterEntry { 23 | /** 24 | * Plugin artifact ID. 25 | */ 26 | @JSONField(name = "name") 27 | public final String artifactId; 28 | /** 29 | * Latest version of this plugin. 30 | */ 31 | private transient final HPI latestOffered; 32 | /** 33 | * Previous version of this plugin. 34 | */ 35 | @CheckForNull 36 | private transient final HPI previousOffered; 37 | 38 | private PluginUpdateCenterEntry(String artifactId, HPI latestOffered, HPI previousOffered) { 39 | this.artifactId = artifactId; 40 | this.latestOffered = latestOffered; 41 | this.previousOffered = previousOffered; 42 | } 43 | 44 | public PluginUpdateCenterEntry(Plugin plugin) throws IOException { 45 | this.artifactId = plugin.getArtifactId(); 46 | HPI previous = null, latest = null; 47 | 48 | Iterator it = plugin.getArtifacts().values().iterator(); 49 | 50 | while (latest == null && it.hasNext()) { 51 | HPI h = it.next(); 52 | try { 53 | h.validate(); 54 | } catch (IOException e) { 55 | LOGGER.log(Level.WARNING, "Failed to resolve "+h+". Dropping this version.",e); 56 | continue; 57 | } 58 | latest = h; 59 | } 60 | 61 | while (previous == null && it.hasNext()) { 62 | HPI h = it.next(); 63 | try { 64 | h.validate(); 65 | } catch (IOException e) { 66 | LOGGER.log(Level.WARNING, "Failed to resolve "+h+". Dropping this version.",e); 67 | continue; 68 | } 69 | previous = h; 70 | } 71 | 72 | if (latest == null) { 73 | throw new IOException("Plugin '" + artifactId + "' has no valid release"); 74 | } 75 | 76 | this.latestOffered = latest; 77 | this.previousOffered = previous == latest ? null : previous; 78 | } 79 | 80 | public PluginUpdateCenterEntry(HPI hpi) { 81 | this(hpi.artifact.artifactId, hpi, null); 82 | } 83 | 84 | /** 85 | * Historical name for the plugin documentation URL field. 86 | * 87 | * Now always links to plugins.jenkins.io, which in turn uses 88 | * {@link io.jenkins.update_center.json.PluginDocumentationUrlsRoot} to determine where the documentation is 89 | * actually located. 90 | * 91 | * @return a URL 92 | */ 93 | @JSONField 94 | public String getWiki() { 95 | return "https://plugins.jenkins.io/" + artifactId; 96 | } 97 | 98 | String getPluginUrl() throws IOException { 99 | return latestOffered.getPluginUrl(); 100 | } 101 | 102 | @JSONField(name = "url") 103 | public URL getDownloadUrl() throws MalformedURLException { 104 | return latestOffered.getDownloadUrl(); 105 | } 106 | 107 | @JSONField(name = "title") 108 | public String getName() throws IOException { 109 | return latestOffered.getName(); 110 | } 111 | 112 | public String getVersion() { 113 | return latestOffered.version; 114 | } 115 | 116 | public String getPreviousVersion() { 117 | return previousOffered == null? null : previousOffered.version; 118 | } 119 | 120 | public String getScm() throws IOException { 121 | return latestOffered.getScmUrl(); 122 | } 123 | 124 | public List getIssueTrackers() { 125 | return IssueTrackerSource.getInstance().getIssueTrackers(artifactId); 126 | } 127 | 128 | public String getRequiredCore() throws IOException { 129 | return latestOffered.getRequiredJenkinsVersion(); 130 | } 131 | 132 | public String getCompatibleSinceVersion() throws IOException { 133 | return latestOffered.getCompatibleSinceVersion(); 134 | } 135 | 136 | public String getBuildDate() throws IOException { 137 | return latestOffered.getTimestampAsString(); 138 | } 139 | 140 | public List getLabels() throws IOException { 141 | List labels = new ArrayList<>(latestOffered.getLabels()); 142 | if (getDevelopers().isEmpty() && !labels.contains("adopt-this-plugin")) { 143 | // Plugins with no maintainers are by definition up for adoption 144 | LOGGER.log(Level.INFO, () -> "Adding 'adopt-this-plugin' label to " + this.artifactId + " due to lack of maintainers"); 145 | labels.add("adopt-this-plugin"); 146 | } 147 | return labels; 148 | } 149 | 150 | public String getDefaultBranch() throws IOException { 151 | return latestOffered.getDefaultBranch(); 152 | } 153 | 154 | public List getDependencies() throws IOException { 155 | return latestOffered.getDependencies(); 156 | } 157 | 158 | public String getSha1() throws IOException { 159 | return latestOffered.getMetadata().sha1; 160 | } 161 | 162 | public String getSha256() throws IOException { 163 | return latestOffered.getMetadata().sha256; 164 | } 165 | 166 | public long getSize() throws IOException { 167 | return latestOffered.getMetadata().size; 168 | } 169 | 170 | public String getGav() { 171 | return latestOffered.getGavId(); 172 | } 173 | 174 | public List getDevelopers() { 175 | return MaintainersSource.getInstance().getMaintainers(this.latestOffered.artifact); 176 | } 177 | 178 | public String getExcerpt() throws IOException { 179 | return latestOffered.getDescription(); 180 | } 181 | 182 | public String getReleaseTimestamp() throws IOException { 183 | return TIMESTAMP_FORMATTER.format(latestOffered.getTimestamp()); 184 | } 185 | 186 | public String getPreviousTimestamp() throws IOException { 187 | return previousOffered == null ? null : TIMESTAMP_FORMATTER.format(previousOffered.getTimestamp()); 188 | } 189 | 190 | public int getPopularity() throws IOException { 191 | return Popularities.getInstance().getPopularity(artifactId); 192 | } 193 | 194 | public Integer getHealth() { 195 | return HealthScores.getInstance().getHealthScore(artifactId); 196 | } 197 | 198 | public String getLatest() { 199 | final LatestPluginVersions instance = LatestPluginVersions.getInstance(); 200 | final VersionNumber latestPublishedVersion = instance.getLatestVersion(artifactId); 201 | if (latestPublishedVersion == null || latestPublishedVersion.equals(latestOffered.getVersion())) { 202 | // only include latest version information if the currently published version isn't the latest 203 | return null; 204 | } 205 | return latestPublishedVersion.toString(); 206 | } 207 | 208 | private static final SimpleDateFormat TIMESTAMP_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.00Z'", Locale.US); 209 | 210 | private static final Logger LOGGER = Logger.getLogger(PluginUpdateCenterEntry.class.getName()); 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/Popularities.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | 5 | import java.io.IOException; 6 | import java.net.URI; 7 | import java.net.http.HttpClient; 8 | import java.net.http.HttpRequest; 9 | import java.net.http.HttpResponse; 10 | import java.util.Map; 11 | import java.util.function.Function; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * Plugin popularity is a unit-less integer value. A larger value means a plugin is more popular. 16 | * The data underlying this definition is undefined, whatever makes sense in context can be used. 17 | *

18 | * This implementation just returns the number of installations at the moment, but that may change at any time. 19 | *

20 | * The first iteration of this class returns decimal (float/double) values, but those caused problems for signature 21 | * validation in Jenkins due to the JSON normalization involved. 22 | */ 23 | public class Popularities { 24 | 25 | private static final String JSON_URL = "https://raw.githubusercontent.com/jenkins-infra/infra-statistics/gh-pages/plugin-installation-trend/latestNumbers.json"; 26 | // or https://stats.jenkins.io/plugin-installation-trend/latestNumbers.json 27 | 28 | private static Popularities instance; 29 | 30 | private final Map popularities; 31 | 32 | private Popularities(Map popularities) { 33 | this.popularities = popularities; 34 | } 35 | 36 | private static void initialize() throws IOException { 37 | HttpRequest request = HttpRequest.newBuilder() 38 | .uri(URI.create(JSON_URL)) 39 | .GET() 40 | .build(); 41 | try (final HttpClient client = HttpClient.newHttpClient()) { 42 | final HttpResponse httpResp = client.send(request, HttpResponse.BodyHandlers.ofString()); 43 | final JsonResponse response = JSON.parseObject(httpResp.body(), JsonResponse.class); 44 | 45 | if (response.plugins == null) { 46 | throw new IllegalArgumentException("Specified popularity URL '" + JSON_URL + "' does not contain a JSON object 'plugins'"); 47 | } 48 | Map popularities = response.plugins.keySet().stream() 49 | .collect(Collectors.toMap(Function.identity(), value -> Integer.valueOf(response.plugins.get(value)))); 50 | instance = new Popularities(popularities); 51 | } catch(InterruptedException e) { 52 | throw new IOException(e); 53 | } 54 | } 55 | 56 | private static class JsonResponse { 57 | public Map plugins; 58 | } 59 | 60 | public static synchronized Popularities getInstance() throws IOException { 61 | if (instance == null) { 62 | initialize(); 63 | } 64 | return instance; 65 | } 66 | 67 | public int getPopularity(String pluginId) { 68 | return this.popularities.getOrDefault(pluginId, 0); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/XmlCache.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import java.io.File; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | public class XmlCache { 8 | public static class CachedValue { 9 | public final String value; 10 | private CachedValue(String value) { 11 | this.value = value; 12 | } 13 | } 14 | 15 | private static Map cache = new HashMap<>(); 16 | 17 | public static CachedValue readCache(File file, String xpath) { 18 | return cache.getOrDefault(file + ":" + xpath, null); 19 | } 20 | 21 | public static void writeCache(File file, String xpath, String value) { 22 | cache.put(file + ":" + xpath, new CachedValue(value)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/args4j/Default.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.args4j; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | /** 7 | * Provide a way to define default values for {@link org.kohsuke.args4j.Option}s 8 | * that cannot be reset in {@link io.jenkins.update_center.Main#run} when taking 9 | * an arguments file. 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface Default { 13 | String value(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/args4j/LevelOptionHandler.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.args4j; 2 | 3 | import org.kohsuke.args4j.CmdLineException; 4 | import org.kohsuke.args4j.CmdLineParser; 5 | import org.kohsuke.args4j.OptionDef; 6 | import org.kohsuke.args4j.spi.OneArgumentOptionHandler; 7 | import org.kohsuke.args4j.spi.Setter; 8 | 9 | import java.util.logging.Level; 10 | 11 | public class LevelOptionHandler extends OneArgumentOptionHandler { 12 | public LevelOptionHandler(CmdLineParser parser, OptionDef option, Setter setter) { 13 | super(parser, option, setter); 14 | } 15 | 16 | @Override 17 | public String getDefaultMetaVariable() { 18 | return "LEVEL"; 19 | } 20 | 21 | @Override 22 | protected Level parse(String s) throws NumberFormatException, CmdLineException { 23 | return Level.parse(s); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/JsonSignature.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | import java.util.List; 6 | 7 | public class JsonSignature { 8 | private List certificates; 9 | 10 | private String digest; 11 | private String signature; 12 | 13 | private String digest512; 14 | private String signature512; 15 | 16 | public void setCertificates(List certificates) { 17 | this.certificates = certificates; 18 | } 19 | 20 | public void setDigest(String digest) { 21 | this.digest = digest; 22 | } 23 | 24 | public void setSignature(String signature) { 25 | this.signature = signature; 26 | } 27 | 28 | public void setDigest512(String digest512) { 29 | this.digest512 = digest512; 30 | } 31 | 32 | public void setSignature512(String signature512) { 33 | this.signature512 = signature512; 34 | } 35 | 36 | public List getCertificates() { 37 | return certificates; 38 | } 39 | 40 | @JSONField(name = "correct_digest") 41 | public String getDigest() { 42 | return digest; 43 | } 44 | 45 | @JSONField(name = "correct_signature") 46 | public String getSignature() { 47 | return signature; 48 | } 49 | 50 | @JSONField(name = "correct_digest512") 51 | public String getDigest512() { 52 | return digest512; 53 | } 54 | 55 | @JSONField(name = "correct_signature512") 56 | public String getSignature512() { 57 | return signature512; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/PlatformCategory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | import java.util.List; 6 | 7 | public class PlatformCategory { 8 | @JSONField 9 | public String category; 10 | 11 | @JSONField 12 | public List plugins; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/PlatformPlugin.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | public class PlatformPlugin { 6 | 7 | /** 8 | * The plugin ID. 9 | */ 10 | @JSONField 11 | public String name; 12 | 13 | /** 14 | * In which version of Jenkins was this suggestion added? 15 | * Used for upgrade wizard / similar functionality. 16 | */ 17 | @JSONField 18 | public String added; 19 | 20 | /** 21 | * Whether this plugin should be installed by default. 22 | */ 23 | @JSONField 24 | public boolean suggested; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/PlatformPluginsRoot.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.annotation.JSONField; 5 | import java.nio.file.Files; 6 | import org.apache.commons.io.IOUtils; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.Reader; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class PlatformPluginsRoot extends WithSignature { 16 | 17 | @JSONField 18 | public List categories; 19 | 20 | public PlatformPluginsRoot(File referenceFile) throws IOException { 21 | try (Reader r = Files.newBufferedReader(referenceFile.toPath(), StandardCharsets.UTF_8)) { 22 | categories = Arrays.asList(JSON.parseObject(IOUtils.toString(r), PlatformCategory[].class)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/PluginDocumentationUrlsRoot.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import io.jenkins.update_center.MavenRepository; 5 | import io.jenkins.update_center.Plugin; 6 | 7 | import java.io.IOException; 8 | import java.util.Map; 9 | import java.util.TreeMap; 10 | 11 | public class PluginDocumentationUrlsRoot extends WithoutSignature { 12 | 13 | @JSONField(unwrapped = true) 14 | public final Map pluginToEntry = new TreeMap<>(); 15 | 16 | public PluginDocumentationUrlsRoot(MavenRepository repo) throws IOException { 17 | for (Plugin plugin : repo.listJenkinsPlugins()) { 18 | pluginToEntry.put(plugin.getArtifactId(), new Entry(plugin.getLatest().getPluginUrl())); 19 | } 20 | } 21 | 22 | public static class Entry { 23 | private final String url; 24 | 25 | private Entry(String url) { 26 | this.url = url; 27 | } 28 | 29 | public String getUrl() { 30 | return url; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/PluginVersions.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import hudson.util.VersionNumber; 5 | import io.jenkins.update_center.HPI; 6 | 7 | import java.io.IOException; 8 | import java.util.Comparator; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import java.util.logging.Logger; 12 | import java.util.stream.Collectors; 13 | 14 | import static java.util.logging.Level.INFO; 15 | 16 | public class PluginVersions { 17 | private static final Logger LOGGER = Logger.getLogger(PluginVersions.class.getName()); 18 | 19 | @JSONField(unwrapped = true) 20 | public Map releases = new LinkedHashMap<>(); 21 | 22 | PluginVersions(Map artifacts) { 23 | for (VersionNumber versionNumber : artifacts.keySet().stream().sorted().collect(Collectors.toList())) { 24 | try { 25 | if (releases.put(versionNumber.toString(), new PluginVersionsEntry(artifacts.get(versionNumber))) != null) { 26 | throw new IllegalStateException("Duplicate key"); 27 | } 28 | } catch (IOException ex) { 29 | LOGGER.log(INFO, "Failed to add " + artifacts.get(versionNumber).artifact.getGav() + " to plugin versions", ex); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/PluginVersionsEntry.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import io.jenkins.update_center.MavenRepository; 5 | import io.jenkins.update_center.HPI; 6 | 7 | import java.io.IOException; 8 | import java.text.SimpleDateFormat; 9 | import java.util.List; 10 | import java.util.Locale; 11 | 12 | public class PluginVersionsEntry { 13 | @JSONField 14 | public final String buildDate; 15 | @JSONField 16 | public final String name; 17 | @JSONField 18 | public final String requiredCore; 19 | @JSONField 20 | public final String sha1; 21 | @JSONField 22 | public final String sha256; 23 | @JSONField 24 | public final String url; 25 | @JSONField 26 | public final String version; 27 | @JSONField 28 | public final String compatibleSinceVersion; 29 | @JSONField 30 | public final String releaseTimestamp; 31 | 32 | @JSONField 33 | public final List dependencies; 34 | 35 | PluginVersionsEntry(HPI hpi) throws IOException { 36 | final MavenRepository.ArtifactMetadata artifactMetadata = hpi.getMetadata(); 37 | name = hpi.artifact.artifactId; 38 | requiredCore = hpi.getRequiredJenkinsVersion(); 39 | sha1 = artifactMetadata.sha1; 40 | sha256 = artifactMetadata.sha256; 41 | url = hpi.getDownloadUrl().toString(); 42 | version = hpi.version; 43 | buildDate = hpi.getTimestampAsString(); 44 | dependencies = hpi.getDependencies(); 45 | compatibleSinceVersion = hpi.getCompatibleSinceVersion(); 46 | releaseTimestamp = TIMESTAMP_FORMATTER.format(hpi.getTimestamp()); 47 | } 48 | 49 | private static final SimpleDateFormat TIMESTAMP_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.00Z'", Locale.US); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/PluginVersionsRoot.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import io.jenkins.update_center.MavenRepository; 5 | import io.jenkins.update_center.Plugin; 6 | 7 | import java.io.IOException; 8 | import java.util.Map; 9 | import java.util.TreeMap; 10 | import java.util.stream.Collectors; 11 | 12 | public class PluginVersionsRoot extends WithSignature { 13 | @JSONField 14 | public final String updateCenterVersion; 15 | private final MavenRepository repository; 16 | 17 | private Map plugins; 18 | 19 | public PluginVersionsRoot(String updateCenterVersion, MavenRepository repository) { 20 | this.updateCenterVersion = updateCenterVersion; 21 | this.repository = repository; 22 | } 23 | 24 | @JSONField 25 | public Map getPlugins() throws IOException { 26 | if (plugins == null) { 27 | plugins = new TreeMap<>(repository.listJenkinsPlugins().stream().collect(Collectors.toMap(Plugin::getArtifactId, plugin -> new PluginVersions(plugin.getArtifacts())))); 28 | } 29 | plugins.entrySet().removeIf(e -> e.getValue().releases.isEmpty()); 30 | return plugins; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/RecentReleasesEntry.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import io.jenkins.update_center.HPI; 4 | 5 | public class RecentReleasesEntry { 6 | private HPI hpi; 7 | public RecentReleasesEntry(HPI hpi) { 8 | this.hpi = hpi; 9 | } 10 | 11 | public String getName() { 12 | return hpi.artifact.artifactId; 13 | } 14 | 15 | public String getVersion() { 16 | return hpi.version; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/RecentReleasesRoot.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import io.jenkins.update_center.HPI; 5 | import io.jenkins.update_center.MavenRepository; 6 | import io.jenkins.update_center.Plugin; 7 | import io.jenkins.update_center.util.Environment; 8 | 9 | import java.io.IOException; 10 | import java.time.Duration; 11 | import java.time.Instant; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class RecentReleasesRoot extends WithoutSignature { 16 | @JSONField 17 | public List releases = new ArrayList<>(); 18 | 19 | public RecentReleasesRoot(MavenRepository repository) throws IOException { 20 | for (Plugin plugin : repository.listJenkinsPlugins()) { 21 | for (HPI release : plugin.getArtifacts().values()) { 22 | if (Instant.ofEpochMilli(release.getTimestamp()).isBefore(Instant.now().minus(MAX_AGE))) { 23 | // too old, ignore 24 | continue; 25 | } 26 | releases.add(new RecentReleasesEntry(release)); 27 | } 28 | } 29 | } 30 | 31 | private static final Duration MAX_AGE = Duration.ofHours(Environment.getInteger("RECENT_RELEASES_MAX_AGE_HOURS", 3)); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/ReleaseHistoryDate.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import io.jenkins.update_center.HPI; 5 | import io.jenkins.update_center.MavenArtifact; 6 | 7 | import java.text.SimpleDateFormat; 8 | import java.util.ArrayList; 9 | import java.util.Date; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | 15 | class ReleaseHistoryDate { 16 | private static final Logger LOGGER = Logger.getLogger(ReleaseHistoryDate.class.getName()); 17 | 18 | @JSONField 19 | public final String date; 20 | 21 | @JSONField 22 | public final List releases; 23 | 24 | ReleaseHistoryDate(Date date, Map pluginsById) { 25 | SimpleDateFormat dateFormat = MavenArtifact.getDateFormat(); 26 | this.date = dateFormat.format(date); 27 | List list = new ArrayList<>(); 28 | for (HPI hpi : pluginsById.values()) { 29 | try { 30 | ReleaseHistoryEntry releaseHistoryEntry = new ReleaseHistoryEntry(hpi); 31 | list.add(releaseHistoryEntry); 32 | } catch (Exception ex) { 33 | LOGGER.log(Level.INFO, "Failed to retrieve plugin info for " + hpi.artifact.artifactId, ex); 34 | } 35 | } 36 | this.releases = list; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/ReleaseHistoryEntry.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import io.jenkins.update_center.HPI; 5 | 6 | import java.io.IOException; 7 | import java.util.Calendar; 8 | import java.util.GregorianCalendar; 9 | 10 | class ReleaseHistoryEntry { 11 | @JSONField 12 | public final String title; 13 | @JSONField 14 | public final String wiki; // historical name for the plugin documentation URL field 15 | @JSONField 16 | public final String gav; 17 | @JSONField 18 | public final String version; 19 | @JSONField 20 | public final long timestamp; 21 | @JSONField 22 | public final String url; 23 | @JSONField 24 | public Boolean latestRelease; 25 | @JSONField 26 | public Boolean firstRelease; 27 | 28 | private static final Calendar DATE_CUTOFF = new GregorianCalendar(); 29 | 30 | static { 31 | DATE_CUTOFF.add(Calendar.DAY_OF_MONTH, -31); 32 | } 33 | 34 | ReleaseHistoryEntry(HPI hpi) throws IOException { 35 | if (hpi.getTimestampAsDate().after(DATE_CUTOFF.getTime())) { 36 | title = hpi.getName(); 37 | wiki = hpi.getPluginUrl(); 38 | } else { 39 | title = null; 40 | wiki = null; 41 | } 42 | if (hpi.getPlugin().getLatest() == hpi) { 43 | latestRelease = true; 44 | } 45 | if (hpi.getPlugin().getFirst() == hpi) { 46 | firstRelease = true; 47 | } 48 | version = hpi.version; 49 | this.gav = hpi.artifact.getGav(); 50 | timestamp = hpi.repository.getMetadata(hpi).timestamp; 51 | url = "https://plugins.jenkins.io/" + hpi.artifact.artifactId; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/ReleaseHistoryRoot.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import io.jenkins.update_center.HPI; 5 | import io.jenkins.update_center.MavenRepository; 6 | 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.Date; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class ReleaseHistoryRoot extends WithoutSignature { 14 | @JSONField 15 | public final List releaseHistory; 16 | 17 | public ReleaseHistoryRoot(MavenRepository repository) throws IOException { 18 | List list = new ArrayList<>(); 19 | for (Map.Entry> entry : repository.listPluginsByReleaseDate().entrySet()) { 20 | ReleaseHistoryDate releaseHistoryDate = new ReleaseHistoryDate(entry.getKey(), entry.getValue()); 21 | list.add(releaseHistoryDate); 22 | } 23 | this.releaseHistory = list; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/TieredUpdateSitesGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2020, Daniel Beck 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.jenkins.update_center.json; 25 | 26 | import com.alibaba.fastjson.annotation.JSONField; 27 | import hudson.util.VersionNumber; 28 | import io.jenkins.update_center.HPI; 29 | import io.jenkins.update_center.JenkinsWar; 30 | import io.jenkins.update_center.MavenRepository; 31 | 32 | import java.io.IOException; 33 | import java.time.Instant; 34 | import java.time.temporal.ChronoUnit; 35 | import java.util.Collection; 36 | import java.util.Comparator; 37 | import java.util.HashSet; 38 | import java.util.List; 39 | import java.util.Objects; 40 | import java.util.Optional; 41 | import java.util.Set; 42 | import java.util.TreeMap; 43 | import java.util.logging.Level; 44 | import java.util.logging.Logger; 45 | import java.util.stream.Collectors; 46 | 47 | public class TieredUpdateSitesGenerator extends WithoutSignature { 48 | 49 | private MavenRepository repository; 50 | 51 | @JSONField 52 | public List weeklyCores; 53 | 54 | @JSONField 55 | public List stableCores; 56 | 57 | public TieredUpdateSitesGenerator withRepository(MavenRepository repository) throws IOException { 58 | this.repository = repository; 59 | update(); 60 | return this; 61 | } 62 | 63 | private static boolean isStableVersion(VersionNumber version) { 64 | return version.getDigitAt(2) != -1; 65 | } 66 | 67 | private static VersionNumber nextWeeklyReleaseAfterStableBaseline(VersionNumber version) { 68 | if (!version.toString().matches("[0-9][.][0-9]+[.][1-9]")) { 69 | throw new IllegalArgumentException("Unexpected LTS version: " + version.toString()); 70 | } 71 | return new VersionNumber(version.getDigitAt(0) + "." + (version.getDigitAt(1) + 1)); 72 | } 73 | 74 | private VersionNumber nextLtsReleaseAfterWeekly(VersionNumber dependencyVersion, Set keySet) { 75 | return keySet.stream().filter(TieredUpdateSitesGenerator::isStableVersion).sorted().filter(v -> v.isNewerThan(dependencyVersion)).findFirst().orElse(null); 76 | } 77 | 78 | private static boolean isReleaseRecentEnough(JenkinsWar war) throws IOException { 79 | Objects.requireNonNull(war, "war"); 80 | return war.getTimestampAsDate().toInstant().isAfter(Instant.now().minus(CORE_AGE_DAYS, ChronoUnit.DAYS)); 81 | } 82 | 83 | public void update() throws IOException { 84 | Collection allPluginReleases = this.repository.listJenkinsPlugins().stream() 85 | .map(plugin -> plugin.getArtifacts().values()) 86 | .reduce(new HashSet<>(), (acc, els) -> { acc.addAll(els); return acc; }); 87 | 88 | final List coreDependencyVersions = allPluginReleases.stream().map(v -> { 89 | try { 90 | return v.getRequiredJenkinsVersion(); 91 | } catch (IOException e) { 92 | LOGGER.log(Level.WARNING, "Failed to determine required Jenkins version for " + v.getGavId(), e); 93 | return null; 94 | } 95 | }).filter(Objects::nonNull).collect(Collectors.toSet()).stream().map(VersionNumber::new).sorted(Comparator.reverseOrder()).collect(Collectors.toList()); 96 | 97 | final TreeMap allJenkinsWarsByVersionNumber = this.repository.getJenkinsWarsByVersionNumber(); 98 | final Set weeklyCores = new HashSet<>(); 99 | final Set stableCores = new HashSet<>(); 100 | 101 | boolean stableDone = false; 102 | boolean weeklyDone = false; 103 | 104 | for (VersionNumber dependencyVersion : coreDependencyVersions) { 105 | final JenkinsWar war = allJenkinsWarsByVersionNumber.get(dependencyVersion); 106 | if (war == null) { 107 | LOGGER.log(Level.INFO, "Did not find declared core dependency version among all core releases: " + dependencyVersion.toString() + ". It is used by " + allPluginReleases.stream().filter( p -> { 108 | try { 109 | return p.getRequiredJenkinsVersion().equals(dependencyVersion.toString()); 110 | } catch (IOException e) { 111 | // ignore 112 | return false; 113 | } 114 | }).map(HPI::getGavId).collect(Collectors.joining(", "))); 115 | continue; 116 | } 117 | final boolean releaseRecentEnough = isReleaseRecentEnough(war); 118 | if (isStableVersion(dependencyVersion)) { 119 | if (!stableDone) { 120 | if (!releaseRecentEnough) { 121 | stableDone = true; 122 | } 123 | stableCores.add(dependencyVersion); 124 | if (!weeklyDone) { 125 | weeklyCores.add(nextWeeklyReleaseAfterStableBaseline(dependencyVersion)); 126 | } 127 | } 128 | } else { 129 | if (!weeklyDone) { 130 | if (!releaseRecentEnough) { 131 | weeklyDone = true; 132 | } 133 | weeklyCores.add(dependencyVersion); 134 | } 135 | // Plugin depends on a weekly version, make sure the next higher LTS release is also included 136 | if (!stableDone) { 137 | final VersionNumber v = nextLtsReleaseAfterWeekly(dependencyVersion, allJenkinsWarsByVersionNumber.keySet()); 138 | if (v != null) { 139 | stableCores.add(v); 140 | } 141 | } 142 | } 143 | if (stableDone && weeklyDone) { 144 | break; 145 | } 146 | } 147 | 148 | this.stableCores = stableCores.stream().map(VersionNumber::toString).sorted().collect(Collectors.toList()); 149 | this.weeklyCores = weeklyCores.stream().map(VersionNumber::toString).sorted().collect(Collectors.toList()); 150 | } 151 | 152 | public static final Logger LOGGER = Logger.getLogger(TieredUpdateSitesGenerator.class.getName()); 153 | 154 | private static final int CORE_AGE_DAYS = 400; 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/UpdateCenterCore.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import hudson.util.VersionNumber; 5 | import io.jenkins.update_center.JenkinsWar; 6 | import io.jenkins.update_center.MavenRepository; 7 | 8 | import java.io.IOException; 9 | import java.util.TreeMap; 10 | 11 | public class UpdateCenterCore { 12 | 13 | @JSONField 14 | public String buildDate; 15 | 16 | @JSONField 17 | public String name = "core"; 18 | 19 | @JSONField 20 | public String sha1; 21 | 22 | @JSONField 23 | public String sha256; 24 | 25 | @JSONField 26 | public String url; 27 | 28 | @JSONField 29 | public String version; 30 | 31 | @JSONField 32 | public long size; 33 | 34 | UpdateCenterCore(TreeMap jenkinsWarsByVersionNumber) throws IOException { 35 | if (jenkinsWarsByVersionNumber.isEmpty()) { 36 | return; 37 | } 38 | 39 | JenkinsWar war = jenkinsWarsByVersionNumber.get(jenkinsWarsByVersionNumber.firstKey()); 40 | 41 | version = war.version; 42 | url = war.getDownloadUrl().toString(); 43 | final MavenRepository.ArtifactMetadata artifactMetadata = war.getMetadata(); 44 | sha1 = artifactMetadata.sha1; 45 | sha256 = artifactMetadata.sha256; 46 | buildDate = war.getTimestampAsString(); 47 | size = artifactMetadata.size; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/UpdateCenterDeprecation.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | public class UpdateCenterDeprecation { 6 | 7 | @JSONField 8 | public final String url; 9 | 10 | public UpdateCenterDeprecation(String url) { 11 | this.url = url; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/UpdateCenterRoot.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.annotation.JSONField; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import io.jenkins.update_center.BaseMavenRepository; 7 | import io.jenkins.update_center.Deprecations; 8 | import io.jenkins.update_center.MavenRepository; 9 | import io.jenkins.update_center.Plugin; 10 | import io.jenkins.update_center.PluginUpdateCenterEntry; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.nio.charset.StandardCharsets; 15 | import java.nio.file.Files; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.TreeMap; 20 | import java.util.function.Function; 21 | import java.util.logging.Level; 22 | import java.util.logging.Logger; 23 | import java.util.stream.Collectors; 24 | import org.apache.commons.lang3.StringUtils; 25 | 26 | public class UpdateCenterRoot extends WithSignature { 27 | private static final Logger LOGGER = Logger.getLogger(UpdateCenterRoot.class.getName()); 28 | 29 | @JSONField 30 | @SuppressFBWarnings(value = "SS_SHOULD_BE_STATIC", justification = "Accessed by JSON serializer") 31 | public final String updateCenterVersion = "1"; 32 | 33 | @JSONField 34 | public final String connectionCheckUrl; 35 | 36 | @JSONField 37 | public final String id; 38 | 39 | @JSONField 40 | public UpdateCenterCore core; 41 | 42 | @JSONField 43 | public Map plugins = new TreeMap<>(); 44 | 45 | @JSONField 46 | public List warnings; 47 | 48 | @JSONField 49 | public Map deprecations; 50 | 51 | public UpdateCenterRoot(String id, String connectionCheckUrl, MavenRepository repo, File warningsJsonFile) throws IOException { 52 | if (StringUtils.isEmpty(id)) { 53 | throw new IllegalArgumentException("'id' is required"); 54 | } 55 | this.id = id; 56 | if (StringUtils.isEmpty(connectionCheckUrl)) { 57 | throw new IllegalArgumentException("'connectionCheckUrl' is required"); 58 | } 59 | this.connectionCheckUrl = connectionCheckUrl; 60 | 61 | // load warnings 62 | final String warningsJsonText = String.join("", Files.readAllLines(warningsJsonFile.toPath(), StandardCharsets.UTF_8)); 63 | warnings = Arrays.asList(JSON.parseObject(warningsJsonText, UpdateCenterWarning[].class)); 64 | 65 | // load deprecations 66 | deprecations = new TreeMap<>(Deprecations.getDeprecatedPlugins().collect(Collectors.toMap(Function.identity(), UpdateCenterRoot::deprecationForPlugin))); 67 | 68 | for (Plugin plugin : repo.listJenkinsPlugins()) { 69 | try { 70 | PluginUpdateCenterEntry entry = new PluginUpdateCenterEntry(plugin); 71 | plugins.put(plugin.getArtifactId(), entry); 72 | } catch (IOException ex) { 73 | LOGGER.log(Level.INFO, "Failed to add update center entry for: " + plugin, ex); 74 | } 75 | } 76 | 77 | core = new UpdateCenterCore(repo.getJenkinsWarsByVersionNumber()); 78 | } 79 | 80 | private static UpdateCenterDeprecation deprecationForPlugin(String artifactId) { 81 | String deprecationUrl = Deprecations.getCustomDeprecationUri(artifactId); 82 | String noticeUrl = deprecationUrl != null ? deprecationUrl : BaseMavenRepository.getIgnoreNoticeUrl(artifactId); 83 | return new UpdateCenterDeprecation(noticeUrl); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/UpdateCenterWarning.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class UpdateCenterWarning { 9 | @JSONField 10 | public String id; 11 | 12 | @JSONField 13 | public String message; 14 | 15 | @JSONField 16 | public String name; 17 | 18 | @JSONField 19 | public String type; 20 | 21 | @JSONField 22 | public String url; 23 | 24 | @JSONField 25 | public List versions = new ArrayList<>(); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/UpdateCenterWarningVersionRange.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | public class UpdateCenterWarningVersionRange { 6 | @JSONField 7 | public String firstVersion; 8 | 9 | @JSONField 10 | public String lastVersion; 11 | 12 | @JSONField 13 | public String pattern; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/WithSignature.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.annotation.JSONField; 5 | import com.alibaba.fastjson.serializer.SerializerFeature; 6 | import io.jenkins.update_center.Signer; 7 | 8 | import io.jenkins.update_center.util.Timestamp; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.io.OutputStream; 12 | import java.io.OutputStreamWriter; 13 | import java.io.StringWriter; 14 | import java.io.Writer; 15 | import java.nio.charset.StandardCharsets; 16 | import java.nio.file.Files; 17 | import java.security.GeneralSecurityException; 18 | 19 | /** 20 | * Support generation of JSON output with included checksum + signatures block for the same JSON output. 21 | */ 22 | public abstract class WithSignature { 23 | private JsonSignature signature; 24 | 25 | @JSONField 26 | public JsonSignature getSignature() { 27 | return signature; 28 | } 29 | 30 | /** 31 | * Returns a string with the current date and time in ISO-8601 format. 32 | * It doesn't have fractional seconds and the timezone is always UTC ('Z'). 33 | * 34 | * @return a string with the current date and time in the format YYYY-MM-DD'T'HH:mm:ss'Z' 35 | */ 36 | public String getGenerationTimestamp() { 37 | return Timestamp.TIMESTAMP; 38 | } 39 | 40 | /** 41 | * Generate JSON checksums and add a signature block to the JSON written to the specified {@link Writer}. 42 | * 43 | * This will run JSON generation twice: Once without the signature block to compute checksums, and a second time to 44 | * include the signature block and write it to the output file. 45 | * 46 | * Because of this, it is important that (with the exception of {@link #getSignature()} all getters etc. of subtypes 47 | * and any types reachable through the object graph for JSON generation return the same content on subsequent calls. 48 | * 49 | * Additionally, implementations of this class, and all types reachable via fields and getters used during JSON 50 | * generation should employ some sort of caching to prevent expensive computations from being invoked twice. 51 | * 52 | * @param writer the writer to write to 53 | * @param signer the signer 54 | * @param pretty whether to pretty-print format the JSON output 55 | * @throws IOException when any IO error occurs 56 | * @throws GeneralSecurityException when an issue during signing occurs 57 | */ 58 | private void writeWithSignature(Writer writer, Signer signer, boolean pretty) throws IOException, GeneralSecurityException { 59 | signature = null; 60 | 61 | final String unsignedJson = JSON.toJSONString(this, SerializerFeature.DisableCircularReferenceDetect); 62 | signature = signer.sign(unsignedJson); 63 | 64 | if (pretty) { 65 | JSON.writeJSONString(writer, this, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.PrettyFormat); 66 | } else { 67 | JSON.writeJSONString(writer, this, SerializerFeature.DisableCircularReferenceDetect); 68 | } 69 | writer.flush(); 70 | } 71 | 72 | /** 73 | * Convenience wrapper for {@link #writeWithSignature(Writer, Signer, boolean)} writing to a file. 74 | * 75 | * @param outputFile the file to write to 76 | * @param signer the signer 77 | * @param pretty whether to pretty-print format the JSON output 78 | * @throws IOException when any IO error occurs 79 | * @throws GeneralSecurityException when an issue during signing occurs 80 | */ 81 | public void writeWithSignature(File outputFile, Signer signer, boolean pretty) throws IOException, GeneralSecurityException { 82 | try (OutputStream os = Files.newOutputStream(outputFile.toPath()); OutputStreamWriter writer = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { 83 | writeWithSignature(writer, signer, pretty); 84 | } 85 | } 86 | 87 | /** 88 | * Like {@link #writeWithSignature(File, Signer, boolean)} but the output is returned as a String. 89 | * @param signer the signer 90 | * @param pretty whether to pretty-print format the JSON output 91 | * @return the JSON output 92 | * @throws IOException when any IO error occurs 93 | * @throws GeneralSecurityException when an issue during signing occurs 94 | */ 95 | public String encodeWithSignature(Signer signer, boolean pretty) throws IOException, GeneralSecurityException { 96 | StringWriter writer = new StringWriter(); 97 | writeWithSignature(writer, signer, pretty); 98 | return writer.getBuffer().toString(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/json/WithoutSignature.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.json; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.serializer.SerializerFeature; 5 | 6 | import java.io.BufferedWriter; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | 12 | public class WithoutSignature { 13 | public void write(File file, boolean pretty) throws IOException { 14 | final File parent = file.getParentFile(); 15 | if (!parent.mkdirs() && !parent.isDirectory()) { 16 | throw new IOException("Failed to create " + parent); 17 | } 18 | try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { 19 | if (pretty) { 20 | JSON.writeJSONString(writer, this, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.PrettyFormat); 21 | } else { 22 | JSON.writeJSONString(writer, this, SerializerFeature.DisableCircularReferenceDetect); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/util/Environment.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.util; 2 | 3 | import javax.annotation.CheckForNull; 4 | import javax.annotation.Nonnull; 5 | import java.util.logging.Level; 6 | import java.util.logging.Logger; 7 | 8 | public final class Environment { 9 | private Environment() {} 10 | 11 | public static String getString(@Nonnull String key, @CheckForNull String defaultValue) { 12 | final String property = System.getProperty(key); 13 | if (property != null) { 14 | LOGGER.log(Level.CONFIG, "Found key: " + key + " in system properties: " + property); 15 | return property; 16 | } 17 | 18 | final String env = System.getenv(key); 19 | if (env != null) { 20 | LOGGER.log(Level.CONFIG, "Found key: " + key + " in process environment: " + env); 21 | return env; 22 | } 23 | 24 | LOGGER.log(Level.CONFIG, "Failed to find key: " + key + " so using default: " + defaultValue); 25 | return defaultValue; 26 | } 27 | 28 | public static String getString(@Nonnull String key) { 29 | return getString(key, null); 30 | } 31 | 32 | public static int getInteger(@Nonnull String key) { 33 | return getInteger(key, 0); 34 | } 35 | 36 | public static int getInteger(@Nonnull String key, int defaultValue) { 37 | try { 38 | return Integer.parseInt(getString(key, Integer.toString(defaultValue))); 39 | } catch (NumberFormatException nfe) { 40 | LOGGER.log(Level.WARNING, nfe.getMessage(), nfe); 41 | return defaultValue; 42 | } 43 | } 44 | 45 | private static final Logger LOGGER = Logger.getLogger(Environment.class.getName()); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/util/Timestamp.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.util; 2 | 3 | import java.time.Instant; 4 | import java.time.ZoneOffset; 5 | import java.time.format.DateTimeFormatter; 6 | 7 | public class Timestamp { 8 | public static final String TIMESTAMP = DateTimeFormatter.ISO_DATE_TIME.format(Instant.now().atOffset(ZoneOffset.UTC).withNano(0)); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/wrappers/AllowedArtifactsListMavenRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.wrappers; 2 | 3 | import hudson.util.VersionNumber; 4 | import io.jenkins.update_center.JenkinsWar; 5 | import io.jenkins.update_center.HPI; 6 | import io.jenkins.update_center.Plugin; 7 | 8 | import java.io.IOException; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Properties; 15 | import java.util.TreeMap; 16 | import java.util.logging.Level; 17 | import java.util.logging.Logger; 18 | import java.util.stream.Collectors; 19 | 20 | public class AllowedArtifactsListMavenRepository extends MavenRepositoryWrapper { 21 | private static final Logger LOGGER = Logger.getLogger(AllowedArtifactsListMavenRepository.class.getName()); 22 | 23 | private final Properties allowedArtifactsList; 24 | 25 | public AllowedArtifactsListMavenRepository(Properties allowedArtifactsList) { 26 | this.allowedArtifactsList = allowedArtifactsList; 27 | } 28 | 29 | @Override 30 | public Collection listJenkinsPlugins() throws IOException { 31 | final Collection plugins = base.listJenkinsPlugins(); 32 | for (Iterator pluginIterator = plugins.iterator(); pluginIterator.hasNext(); ) { 33 | Plugin plugin = pluginIterator.next(); 34 | final String listEntry = allowedArtifactsList.getProperty(plugin.getArtifactId()); 35 | 36 | if (listEntry == null) { 37 | pluginIterator.remove(); 38 | continue; 39 | } 40 | 41 | if (listEntry.equals("*")) { 42 | continue; // entire artifactId allowed 43 | } 44 | 45 | final List allowedVersions = Arrays.stream(listEntry.split("\\s+")).map(String::trim).collect(Collectors.toList()); 46 | 47 | for (Iterator> versionIterator = plugin.getArtifacts().entrySet().iterator(); versionIterator.hasNext(); ) { 48 | Map.Entry entry = versionIterator.next(); 49 | HPI hpi = entry.getValue(); 50 | if (!allowedVersions.contains(hpi.version)) { 51 | versionIterator.remove(); 52 | } 53 | } 54 | if (plugin.getArtifacts().isEmpty()) { 55 | LOGGER.log(Level.WARNING, "Individual versions of a plugin are allowed, but none of them matched: " + plugin.getArtifactId() + " versions: " + listEntry); 56 | pluginIterator.remove(); 57 | } 58 | } 59 | return plugins; 60 | } 61 | 62 | @Override 63 | public TreeMap getJenkinsWarsByVersionNumber() throws IOException { 64 | final String listEntry = allowedArtifactsList.getProperty("jenkins-core"); 65 | 66 | if (listEntry == null) { 67 | return new TreeMap<>(); // TODO fix return type so it's only a Map 68 | } 69 | 70 | TreeMap releases = base.getJenkinsWarsByVersionNumber(); 71 | 72 | if (listEntry.equals("*")) { 73 | return releases; 74 | } 75 | 76 | final List allowedVersions = Arrays.stream(listEntry.split("\\s+")).map(String::trim).collect(Collectors.toList()); 77 | 78 | releases.keySet().retainAll(releases.keySet().stream().filter(it -> allowedVersions.contains(it.toString())).collect(Collectors.toSet())); 79 | 80 | return releases; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/wrappers/AlphaBetaOnlyRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.wrappers; 2 | 3 | import hudson.util.VersionNumber; 4 | import io.jenkins.update_center.HPI; 5 | import io.jenkins.update_center.Plugin; 6 | 7 | import java.io.IOException; 8 | import java.util.Collection; 9 | import java.util.Iterator; 10 | import java.util.Map.Entry; 11 | 12 | /** 13 | * Filter down to alpha/beta releases of plugins (or the negation of it.) 14 | * 15 | * @author Kohsuke Kawaguchi 16 | */ 17 | public class AlphaBetaOnlyRepository extends MavenRepositoryWrapper { 18 | 19 | /** 20 | * If true, negate the logic and only find non-alpha/beta releases. 21 | */ 22 | private boolean negative; 23 | 24 | public AlphaBetaOnlyRepository(boolean negative) { 25 | this.negative = negative; 26 | } 27 | 28 | @Override 29 | public Collection listJenkinsPlugins() throws IOException { 30 | Collection r = base.listJenkinsPlugins(); 31 | for (Iterator jtr = r.iterator(); jtr.hasNext();) { 32 | Plugin h = jtr.next(); 33 | 34 | for (Iterator> itr = h.getArtifacts().entrySet().iterator(); itr.hasNext();) { 35 | Entry e = itr.next(); 36 | if (e.getValue().isAlphaOrBeta()^negative) 37 | continue; 38 | itr.remove(); 39 | } 40 | 41 | if (h.getArtifacts().isEmpty()) 42 | jtr.remove(); 43 | } 44 | 45 | return r; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/wrappers/FilteringRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.wrappers; 2 | 3 | import hudson.util.VersionNumber; 4 | import io.jenkins.update_center.PluginFilter; 5 | import io.jenkins.update_center.HPI; 6 | import io.jenkins.update_center.Plugin; 7 | 8 | import javax.annotation.Nonnull; 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public class FilteringRepository extends MavenRepositoryWrapper { 17 | 18 | /** 19 | * Adds a plugin filter. 20 | * @param filter Filter to be added. 21 | */ 22 | private void addPluginFilter(@Nonnull PluginFilter filter) { 23 | pluginFilters.add(filter); 24 | } 25 | 26 | private List pluginFilters = new ArrayList<>(); 27 | 28 | @Override 29 | public Collection listJenkinsPlugins() throws IOException { 30 | Collection r = base.listJenkinsPlugins(); 31 | for (Iterator jtr = r.iterator(); jtr.hasNext();) { 32 | Plugin h = jtr.next(); 33 | 34 | for (Iterator> itr = h.getArtifacts().entrySet().iterator(); itr.hasNext();) { 35 | Map.Entry e = itr.next(); 36 | for (PluginFilter filter : pluginFilters) { 37 | if (filter.shouldIgnore(e.getValue())) { 38 | itr.remove(); 39 | } 40 | } 41 | } 42 | 43 | if (h.getArtifacts().isEmpty()) 44 | jtr.remove(); 45 | } 46 | 47 | return r; 48 | } 49 | 50 | public FilteringRepository withPluginFilter(PluginFilter pluginFilter) { 51 | addPluginFilter(pluginFilter); 52 | return this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/wrappers/MavenRepositoryWrapper.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.wrappers; 2 | 3 | import hudson.util.VersionNumber; 4 | import io.jenkins.update_center.MavenRepository; 5 | import io.jenkins.update_center.ArtifactCoordinates; 6 | import io.jenkins.update_center.JenkinsWar; 7 | import io.jenkins.update_center.MavenArtifact; 8 | import io.jenkins.update_center.Plugin; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.util.Collection; 14 | import java.util.Map; 15 | import java.util.TreeMap; 16 | import java.util.jar.Manifest; 17 | 18 | public class MavenRepositoryWrapper implements MavenRepository { 19 | 20 | MavenRepository base; 21 | 22 | public MavenRepositoryWrapper withBaseRepository(MavenRepository base) { 23 | this.base = base; 24 | return this; 25 | } 26 | 27 | @Override 28 | public TreeMap getJenkinsWarsByVersionNumber() throws IOException { 29 | return base.getJenkinsWarsByVersionNumber(); 30 | } 31 | 32 | @Override 33 | public void addWarsInGroupIdToMap(Map r, String groupId, VersionNumber cap) throws IOException { 34 | base.addWarsInGroupIdToMap(r, groupId, cap); 35 | } 36 | 37 | @Override 38 | public Collection listAllPlugins() throws IOException { 39 | return base.listAllPlugins(); 40 | } 41 | 42 | @Override 43 | public ArtifactMetadata getMetadata(MavenArtifact artifact) throws IOException { 44 | return base.getMetadata(artifact); 45 | } 46 | 47 | @Override 48 | public Manifest getManifest(MavenArtifact artifact) throws IOException { 49 | return base.getManifest(artifact); 50 | } 51 | 52 | @Override 53 | public InputStream getZipFileEntry(MavenArtifact artifact, String path) throws IOException { 54 | return base.getZipFileEntry(artifact, path); 55 | } 56 | 57 | @Override 58 | public File resolve(ArtifactCoordinates artifact) throws IOException { 59 | return base.resolve(artifact); 60 | } 61 | 62 | @Override 63 | public Collection listJenkinsPlugins() throws IOException { 64 | return base.listJenkinsPlugins(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/wrappers/StableWarMavenRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.wrappers; 2 | 3 | import hudson.util.VersionNumber; 4 | import io.jenkins.update_center.JenkinsWar; 5 | 6 | import java.io.IOException; 7 | import java.util.TreeMap; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * Delegating {@link MavenRepositoryWrapper} to limit core releases to those with LTS version numbers. 12 | */ 13 | public class StableWarMavenRepository extends MavenRepositoryWrapper { 14 | 15 | @Override 16 | public TreeMap getJenkinsWarsByVersionNumber() throws IOException { 17 | TreeMap releases = base.getJenkinsWarsByVersionNumber(); 18 | 19 | releases.keySet().retainAll(releases.keySet().stream().filter(it -> it.getDigitAt(2) != -1).collect(Collectors.toSet())); 20 | 21 | return releases; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/wrappers/TruncatedMavenRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.wrappers; 2 | 3 | import io.jenkins.update_center.MavenRepository; 4 | import io.jenkins.update_center.Plugin; 5 | 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.List; 10 | 11 | /** 12 | * {@link MavenRepository} that limits the # of plugins that it reports. 13 | * 14 | * This is primary for monkey-testing with subset of data. 15 | */ 16 | public class TruncatedMavenRepository extends MavenRepositoryWrapper { 17 | private final int cap; 18 | 19 | public TruncatedMavenRepository(int cap) { 20 | this.cap = cap; 21 | } 22 | 23 | @Override 24 | public Collection listJenkinsPlugins() throws IOException { 25 | List result = new ArrayList<>(base.listJenkinsPlugins()); 26 | return result.subList(0, Math.min(cap,result.size())); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/update_center/wrappers/VersionCappedMavenRepository.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center.wrappers; 2 | 3 | import hudson.util.VersionNumber; 4 | import io.jenkins.update_center.JenkinsWar; 5 | import io.jenkins.update_center.BaseMavenRepository; 6 | import io.jenkins.update_center.HPI; 7 | import io.jenkins.update_center.Plugin; 8 | 9 | import javax.annotation.CheckForNull; 10 | import java.io.IOException; 11 | import java.util.Collection; 12 | import java.util.Iterator; 13 | import java.util.Map; 14 | import java.util.Map.Entry; 15 | import java.util.TreeMap; 16 | import java.util.logging.Level; 17 | 18 | /** 19 | * Delegating {@link BaseMavenRepository} to limit the data to the subset compatible with the specific version. 20 | */ 21 | public class VersionCappedMavenRepository extends MavenRepositoryWrapper { 22 | 23 | /** 24 | * Version number to cap. We only report plugins that are compatible with this core version. 25 | */ 26 | @CheckForNull 27 | private final VersionNumber capPlugin; 28 | 29 | /** 30 | * Version number to cap core. We only report core versions as high as this. 31 | */ 32 | @CheckForNull 33 | private final VersionNumber capCore; 34 | 35 | public VersionCappedMavenRepository(@CheckForNull VersionNumber capPlugin, @CheckForNull VersionNumber capCore) { 36 | this.capPlugin = capPlugin; 37 | this.capCore = capCore; 38 | } 39 | 40 | @Override 41 | public TreeMap getJenkinsWarsByVersionNumber() throws IOException { 42 | final TreeMap allWars = base.getJenkinsWarsByVersionNumber(); 43 | if (capCore == null) { 44 | return allWars; 45 | } 46 | return new TreeMap<>(allWars.tailMap(capCore,true)); 47 | } 48 | 49 | @Override 50 | public Collection listJenkinsPlugins() throws IOException { 51 | Collection r = base.listJenkinsPlugins(); 52 | 53 | for (Iterator jtr = r.iterator(); jtr.hasNext();) { 54 | Plugin h = jtr.next(); 55 | 56 | 57 | Map versionNumberHPIMap = new TreeMap<>(VersionNumber.DESCENDING); 58 | 59 | for (Entry e : h.getArtifacts().entrySet()) { 60 | if (capPlugin == null) { 61 | // no cap 62 | versionNumberHPIMap.put(e.getKey(), e.getValue()); 63 | if (versionNumberHPIMap.size() >= 2) { 64 | break; 65 | } 66 | continue; 67 | } 68 | try { 69 | VersionNumber v = new VersionNumber(e.getValue().getRequiredJenkinsVersion()); 70 | if (v.compareTo(capPlugin) <= 0) { 71 | versionNumberHPIMap.put(e.getKey(), e.getValue()); 72 | if (versionNumberHPIMap.size() >= 2) { 73 | break; 74 | } 75 | } 76 | } catch (IOException x) { 77 | LOGGER.log(Level.WARNING, "Failed to filter version " + e.getKey() + " by core dependency for plugin: " + h.getArtifactId(), x); 78 | } 79 | } 80 | 81 | h.getArtifacts().entrySet().retainAll(versionNumberHPIMap.entrySet()); 82 | 83 | if (h.getArtifacts().isEmpty()) 84 | jtr.remove(); 85 | } 86 | 87 | return r; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/DummyTest.java: -------------------------------------------------------------------------------- 1 | import hudson.util.VersionNumber; 2 | import junit.framework.TestCase; 3 | 4 | /** 5 | * @author Kohsuke Kawaguchi 6 | */ 7 | public class DummyTest extends TestCase { 8 | public void test1() {} // work around a bug in surefire plugin 9 | 10 | public void test2() { 11 | assertTrue(new VersionNumber("1.0-alpha-1").compareTo(new VersionNumber("1.0"))<0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/ListExisting.java: -------------------------------------------------------------------------------- 1 | import io.jenkins.update_center.DefaultMavenRepositoryBuilder; 2 | import io.jenkins.update_center.HPI; 3 | import io.jenkins.update_center.MavenRepository; 4 | import io.jenkins.update_center.Plugin; 5 | 6 | import java.util.Collection; 7 | import java.util.Set; 8 | import java.util.TreeSet; 9 | 10 | /** 11 | * List up existing groupIds used by plugins. 12 | * 13 | * @author Kohsuke Kawaguchi 14 | */ 15 | public class ListExisting { 16 | public static void main(String[] args) throws Exception{ 17 | MavenRepository r = DefaultMavenRepositoryBuilder.getInstance(); 18 | 19 | Set groupIds = new TreeSet(); 20 | Collection all = r.listJenkinsPlugins(); 21 | for (Plugin p : all) { 22 | HPI hpi = p.getLatest(); 23 | groupIds.add(hpi.artifact.groupId); 24 | } 25 | 26 | for (String groupId : groupIds) { 27 | System.out.println(groupId); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/ListPluginsAndVersions.java: -------------------------------------------------------------------------------- 1 | import io.jenkins.update_center.DefaultMavenRepositoryBuilder; 2 | import io.jenkins.update_center.HPI; 3 | import io.jenkins.update_center.MavenRepository; 4 | import io.jenkins.update_center.Plugin; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * Test program that lists all the plugin names and their versions. 10 | * 11 | * @author Kohsuke Kawaguchi 12 | */ 13 | public class ListPluginsAndVersions { 14 | public static void main(String[] args) throws Exception{ 15 | MavenRepository r = DefaultMavenRepositoryBuilder.getInstance(); 16 | 17 | System.out.println(r.getJenkinsWarsByVersionNumber().firstKey()); 18 | 19 | Collection all = r.listJenkinsPlugins(); 20 | for (Plugin p : all) { 21 | HPI hpi = p.getLatest(); 22 | System.out.printf("%s\t%s\n", p.getArtifactId(), hpi.toString()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/ArtifactCoordinatesTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class ArtifactCoordinatesTest { 8 | 9 | @Test 10 | public void testVersionValid() { 11 | assertVersionIsValid("2", true); 12 | assertVersionIsValid("3.1-rc4", true); 13 | assertVersionIsValid("4-beta", true); 14 | assertVersionIsValid("2g", false); 15 | assertVersionIsValid("g2", false); 16 | assertVersionIsValid("-2", false); 17 | } 18 | 19 | private void assertVersionIsValid(String version, boolean valid) { 20 | ArtifactCoordinates coordinates = new ArtifactCoordinates("", "", version, ""); 21 | assertEquals(valid, coordinates.isVersionValid()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/DeprecationTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.file.Files; 9 | import java.util.Properties; 10 | 11 | import static org.junit.Assert.assertFalse; 12 | 13 | public class DeprecationTest { 14 | 15 | @Test 16 | public void noOverlap() throws IOException { 17 | Properties ignore = new Properties(); 18 | InputStream stream = Files.newInputStream(new File(Main.resourcesDir, 19 | "artifact-ignores.properties").toPath()); 20 | ignore.load(stream); 21 | Properties deprecations = new Properties(); 22 | stream = Files.newInputStream(new File(Main.resourcesDir, 23 | "deprecations.properties").toPath()); 24 | deprecations.load(stream); 25 | for (String key: deprecations.stringPropertyNames()) { 26 | assertFalse(key, ignore.containsKey(key)); 27 | } 28 | for (String key: ignore.stringPropertyNames()) { 29 | assertFalse(key, deprecations.containsKey(key)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/GitHubSourceTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import okhttp3.mockwebserver.MockResponse; 4 | import okhttp3.mockwebserver.MockWebServer; 5 | import org.apache.commons.io.IOUtils; 6 | import org.junit.Test; 7 | 8 | import java.io.IOException; 9 | import java.util.Arrays; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | public class GitHubSourceTest { 14 | 15 | @Test 16 | public void testCodeQL() throws Exception { 17 | MockWebServer server = new MockWebServer(); 18 | 19 | server.enqueue(new MockResponse().setBody( 20 | IOUtils.toString( 21 | this.getClass().getClassLoader().getResourceAsStream("github_graphql_null.txt"), 22 | "UTF-8" 23 | ) 24 | )); 25 | server.enqueue(new MockResponse().setBody( 26 | IOUtils.toString( 27 | this.getClass().getClassLoader().getResourceAsStream("github_graphql_Y3Vyc29yOnYyOpHOA0oRaA==.txt"), 28 | "UTF-8" 29 | ) 30 | )); 31 | // Start the server. 32 | server.start(); 33 | 34 | GitHubSource gh = new MockWebServerGitHubSource(server); 35 | assertEquals(Arrays.asList("cmake","jenkins-plugin", "jenkins-builder", "pipeline"), gh.getRepositoryTopics("jenkinsci", "cmakebuilder-plugin")); 36 | assertEquals("incoming", gh.getDefaultBranch("jenkinsci", "jmdns")); 37 | // Shut down the server. Instances cannot be reused. 38 | server.shutdown(); 39 | } 40 | 41 | private static class MockWebServerGitHubSource extends GitHubSource { 42 | private final MockWebServer server; 43 | 44 | private MockWebServerGitHubSource(MockWebServer server) throws IOException { 45 | this.server = server; 46 | initializeOrganizationData("jenkinsci"); 47 | } 48 | 49 | @Override 50 | protected String getGraphqlUrl() { 51 | return server.url("/graphql").toString(); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/HPITest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertFalse; 6 | import static org.junit.Assert.assertTrue; 7 | 8 | public class HPITest { 9 | @Test 10 | public void isValidCoreDependencyTest() { 11 | assertTrue(HPI.isValidCoreDependency("1.0")); 12 | assertTrue(HPI.isValidCoreDependency("1.654")); 13 | assertTrue(HPI.isValidCoreDependency("2.0")); 14 | assertTrue(HPI.isValidCoreDependency("2.1")); 15 | assertTrue(HPI.isValidCoreDependency("2.1000")); 16 | assertFalse(HPI.isValidCoreDependency("2.00")); 17 | assertFalse(HPI.isValidCoreDependency("2.01")); 18 | assertFalse(HPI.isValidCoreDependency("2.100-SNAPSHOT")); 19 | assertFalse(HPI.isValidCoreDependency("2.0-rc-1")); 20 | assertFalse(HPI.isValidCoreDependency("2.0-rc-1.vabcd1234")); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/JsonChecksumTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.annotation.JSONField; 5 | import com.alibaba.fastjson.serializer.SerializerFeature; 6 | import net.sf.json.JSONArray; 7 | import net.sf.json.JSONObject; 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | 11 | import java.io.StringWriter; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class JsonChecksumTest { 16 | @Test 17 | public void testJsonChecksum() throws Exception { 18 | 19 | // step 1: Generate JSON using fastjson 20 | Root root = new Root(); 21 | 22 | StringWriter fastJsonWriter = new StringWriter(); 23 | JSON.writeJSONString(fastJsonWriter, root, SerializerFeature.DisableCircularReferenceDetect); 24 | String fastJsonOutput = fastJsonWriter.getBuffer().toString(); 25 | 26 | // step 2: Load generated JSON with json-lib, re-write it canonically 27 | JSONObject o = JSONObject.fromObject(fastJsonOutput); 28 | 29 | StringWriter jsonLibWriter = new StringWriter(); 30 | o.writeCanonical(jsonLibWriter); 31 | String jsonLibOutput = jsonLibWriter.getBuffer().toString(); 32 | 33 | Assert.assertEquals("JSONlib and fastjson should generate same output", jsonLibOutput, fastJsonOutput); 34 | 35 | // step 3: Generate equivalent JSON with json-lib, compare 36 | StringWriter jsonLibWriter2 = new StringWriter(); 37 | root.toJSON().writeCanonical(jsonLibWriter2); 38 | String jsonLibOutput2 = jsonLibWriter2.getBuffer().toString(); 39 | 40 | Assert.assertEquals("JSONlib after load and standalone should be the same", jsonLibOutput, jsonLibOutput2); 41 | } 42 | 43 | private static class Root { 44 | @JSONField 45 | public String baz; 46 | 47 | @JSONField 48 | public List entries; 49 | 50 | @JSONField 51 | public String foo; 52 | 53 | @JSONField 54 | public String bar; 55 | 56 | @JSONField 57 | public long getLong() { 58 | return Long.MAX_VALUE; 59 | }; 60 | 61 | @JSONField 62 | public float random; 63 | 64 | public Root() { 65 | random = (float) Math.random(); 66 | entries = new ArrayList<>(); 67 | entries.add(new ListEntry("one")); 68 | entries.add(new ListEntry("two")); 69 | entries.add(new ListEntry("three")); 70 | entries.add(new ListEntry("four")); 71 | 72 | bar = "bar"; 73 | baz = "qux"; 74 | foo = "Quuux"; 75 | } 76 | 77 | public JSONObject toJSON() { 78 | JSONObject o = new JSONObject(); 79 | o.put("bar", bar); 80 | o.put("foo", foo); 81 | o.put("baz", baz); 82 | o.put("random", random); 83 | o.put("long", getLong()); 84 | JSONArray a = new JSONArray(); 85 | entries.forEach(e -> a.add(e.toJSON())); 86 | o.put("entries", a); 87 | return o; 88 | } 89 | } 90 | 91 | private static class ListEntry { 92 | @JSONField 93 | public String value; 94 | public ListEntry(String value) { 95 | this.value = value; 96 | } 97 | 98 | @JSONField 99 | public String getFoo() { 100 | return "\u0000"; 101 | } 102 | @JSONField 103 | public String getBar() { 104 | return "\uD834\uDD1E"; 105 | } 106 | 107 | public JSONObject toJSON() { 108 | JSONObject o = new JSONObject(); 109 | o.put("value", value); 110 | o.put("foo", getFoo()); 111 | o.put("bar", getBar()); 112 | return o; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/MaintainersSourceTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.util.List; 7 | 8 | public class MaintainersSourceTest { 9 | @Test 10 | public void testContent() { 11 | final List maintainers = MaintainersSource.getInstance().getMaintainers(new ArtifactCoordinates("org.jenkins-ci.plugins", "matrix-auth", "unused", "unused")); 12 | Assert.assertEquals("matrix-auth has one maintainer", 1, maintainers.size()); 13 | final MaintainersSource.Maintainer maintainer = maintainers.get(0); 14 | Assert.assertEquals("User ID expected", "danielbeck", maintainer.getDeveloperId()); 15 | Assert.assertEquals("Display name expected", "Daniel Beck", maintainer.getName()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/SanitizerTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import org.junit.Test; 4 | import org.owasp.html.HtmlSanitizer; 5 | import org.owasp.html.HtmlStreamRenderer; 6 | 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | import static org.hamcrest.Matchers.allOf; 11 | import static org.hamcrest.Matchers.containsString; 12 | import static org.hamcrest.Matchers.is; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | 15 | public class SanitizerTest { 16 | private static final Logger LOGGER = Logger.getLogger(SanitizerTest.class.getName()); 17 | 18 | @Test 19 | public void testSanitizer() { 20 | assertSanitize("strong!", "strong!"); 21 | assertSanitize("foo", "foo"); 22 | assertSanitize("this is the logo:", "this is the logo:"); 23 | assertSanitize("this is the URL", "this is the URL"); 24 | assertSanitize("this is the URL", "this is the URL"); 25 | assertSanitize("this is the URL", "this is the URL"); 26 | assertSanitize("this is the URL", "this is the URL"); 27 | } 28 | 29 | private void assertSanitize(String expected, String input) { 30 | StringBuilder b = new StringBuilder(); 31 | HtmlStreamRenderer renderer = HtmlStreamRenderer.create(b, Throwable::printStackTrace, html -> LOGGER.log(Level.INFO, "Bad HTML: '" + html + "'")); 32 | HtmlSanitizer.sanitize(input, HPI.HTML_POLICY.apply(renderer), HPI.PRE_PROCESSOR); 33 | assertSanitizer336Workaround(expected, b.toString()); 34 | } 35 | 36 | // Workaround for https://github.com/OWASP/java-html-sanitizer/issues/336 37 | private void assertSanitizer336Workaround(String expected, String actual) { 38 | if (expected.contains("rel")) { 39 | assertThat(expected, allOf(containsString("nofollow"), containsString("noopener"), containsString("noreferrer"))); 40 | assertThat(actual, allOf(containsString("nofollow"), containsString("noopener"), containsString("noreferrer"))); 41 | } 42 | expected = expected.replace("nofollow", "*").replace("noopener", "*").replace("noreferrer", "*"); 43 | actual = actual.replace("nofollow", "*").replace("noopener", "*").replace("noreferrer", "*"); 44 | assertThat(expected, is(actual)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/SignerTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import io.jenkins.update_center.json.JsonSignature; 4 | import java.nio.file.StandardCopyOption; 5 | import java.util.Collections; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.security.GeneralSecurityException; 11 | import org.junit.Test; 12 | 13 | import static org.hamcrest.CoreMatchers.is; 14 | import static org.hamcrest.CoreMatchers.notNullValue; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | 17 | // The resources used in these tests expire in 2034. 18 | // Generated with: 19 | // OpenSSL 3.3.2 3 Sep 2024 (Library: OpenSSL 3.3.2 3 Sep 2024) 20 | // Using: 21 | // openssl genrsa [-traditional] -out FILENAME.key 4096 22 | // openssl req -new -x509 -days 3650 -key FILENAME.key -out FILENAME.cert -subj "/C=/ST=/L=/O=SignerTest/OU=SignerTest/CN=SignerTest/emailAddress=example@example.invalid" 23 | public class SignerTest { 24 | @Test 25 | public void traditionalFormat() throws IOException, GeneralSecurityException { 26 | Signer signer = new Signer(); 27 | try (InputStream is = SignerTest.class.getResourceAsStream("/traditional.key")) { 28 | final Path filePath = Files.createTempFile("update-center2-", ".key"); 29 | Files.copy(is, filePath, StandardCopyOption.REPLACE_EXISTING); 30 | signer.privateKey = filePath.toFile(); 31 | } 32 | try (InputStream is = SignerTest.class.getResourceAsStream("/traditional.cert")) { 33 | final Path filePath = Files.createTempFile("update-center2-", ".cert"); 34 | Files.copy(is, filePath, StandardCopyOption.REPLACE_EXISTING); 35 | signer.certificates = Collections.singletonList(filePath.toFile()); 36 | } 37 | 38 | final JsonSignature signature = signer.sign("{}"); 39 | assertThat(signature.getSignature512(), notNullValue()); 40 | assertThat(signature.getDigest512(), notNullValue()); 41 | assertThat(signature.getSignature(), notNullValue()); 42 | assertThat(signature.getCertificates().size(), is(1)); 43 | } 44 | 45 | @Test 46 | public void modernFormat() throws IOException, GeneralSecurityException { 47 | Signer signer = new Signer(); 48 | try (InputStream is = SignerTest.class.getResourceAsStream("/modern.key")) { 49 | final Path filePath = Files.createTempFile("update-center2-", ".key"); 50 | Files.copy(is, filePath, StandardCopyOption.REPLACE_EXISTING); 51 | signer.privateKey = filePath.toFile(); 52 | } 53 | try (InputStream is = SignerTest.class.getResourceAsStream("/modern.cert")) { 54 | final Path filePath = Files.createTempFile("update-center2-", ".cert"); 55 | Files.copy(is, filePath, StandardCopyOption.REPLACE_EXISTING); 56 | signer.certificates = Collections.singletonList(filePath.toFile()); 57 | } 58 | 59 | final JsonSignature signature = signer.sign("{}"); 60 | assertThat(signature.getSignature512(), notNullValue()); 61 | assertThat(signature.getDigest512(), notNullValue()); 62 | assertThat(signature.getSignature(), notNullValue()); 63 | assertThat(signature.getCertificates().size(), is(1)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/TimestampTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import io.jenkins.update_center.json.WithSignature; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.time.Instant; 8 | import java.time.temporal.ChronoUnit; 9 | 10 | public class TimestampTest { 11 | @Test 12 | public void checkTimestamp() throws Exception { 13 | WithSignature w = new WithSignature() { 14 | }; 15 | 16 | String timestamp = w.getGenerationTimestamp(); 17 | 18 | Assert.assertTrue("format as expected", timestamp.matches("^202[0-9][-][0-9]{2}[-][0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")); 19 | 20 | Instant parsed = Instant.parse(timestamp); 21 | 22 | Assert.assertTrue("before now", parsed.isBefore(Instant.now())); 23 | Assert.assertTrue("very recent", parsed.isAfter(Instant.now().minus(1, ChronoUnit.SECONDS))); 24 | 25 | String timestamp2 = w.getGenerationTimestamp(); 26 | Assert.assertEquals("Doesn't change over time", timestamp, timestamp2); 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/update_center/WarningsTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.update_center; 2 | 3 | import junit.framework.Assert; 4 | import net.sf.json.JSONArray; 5 | import net.sf.json.JSONException; 6 | import net.sf.json.JSONObject; 7 | import org.apache.commons.io.IOUtils; 8 | import org.junit.Test; 9 | 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.IOException; 13 | import java.net.URI; 14 | import java.net.http.HttpClient; 15 | import java.net.http.HttpRequest; 16 | import java.net.http.HttpResponse; 17 | import java.nio.file.Files; 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.regex.Pattern; 23 | 24 | public class WarningsTest { 25 | @Test 26 | public void testValidJsonFile() throws Exception { 27 | String warningsText = IOUtils.toString(new FileInputStream(new File("resources/warnings.json"))); 28 | JSONArray warnings = JSONArray.fromObject(warningsText); 29 | 30 | for (int i = 0 ; i < warnings.size() ; i++) { 31 | JSONObject o = warnings.getJSONObject(i); 32 | assertNonEmptyString(o.getString("id")); 33 | assertNonEmptyString(o.getString("type")); 34 | assertNonEmptyString(o.getString("name")); 35 | assertNonEmptyString(o.getString("message")); 36 | assertNonEmptyString(o.getString("url")); 37 | JSONArray versions = o.getJSONArray("versions"); 38 | for (int j = 0 ; j < versions.size() ; j++) { 39 | JSONObject version = versions.getJSONObject(j); 40 | String pattern = version.getString("pattern"); 41 | assertNonEmptyString(pattern); 42 | Pattern p = Pattern.compile(pattern); 43 | } 44 | } 45 | } 46 | 47 | private void assertNonEmptyString(String str) { 48 | Assert.assertNotNull(str); 49 | Assert.assertFalse("".equals(str)); 50 | } 51 | 52 | static class Warning { 53 | public String id; 54 | public Map versions = new HashMap<>(); 55 | } 56 | 57 | private Map> loadPluginWarnings() throws IOException { 58 | Map> loadedWarnings = new HashMap<>(); 59 | 60 | String warningsText = IOUtils.toString(Files.newInputStream(new File(Main.resourcesDir, "warnings.json").toPath())); 61 | JSONArray warnings = JSONArray.fromObject(warningsText); 62 | 63 | for (int i = 0 ; i < warnings.size() ; i++) { 64 | JSONObject o = warnings.getJSONObject(i); 65 | 66 | if (o.getString("type").equals("core")) { 67 | continue; 68 | } 69 | 70 | Warning warning = new Warning(); 71 | warning.id = o.getString("url") + " / " + o.getString("id"); 72 | 73 | JSONArray versions = o.getJSONArray("versions"); 74 | for (int j = 0 ; j < versions.size() ; j++) { 75 | JSONObject version = versions.getJSONObject(j); 76 | String pattern = version.getString("pattern"); 77 | assertNonEmptyString(pattern); 78 | 79 | if (pattern.contains("beta")) { 80 | // ignore for this test as these don't show in release history but we don't have an experimental release history 81 | continue; 82 | } 83 | 84 | Pattern p = Pattern.compile(pattern); 85 | warning.versions.put(p, false); 86 | } 87 | 88 | if (!loadedWarnings.containsKey(o.getString("name"))) { 89 | loadedWarnings.put(o.getString("name"), new ArrayList()); 90 | } 91 | loadedWarnings.get(o.getString("name")).add(warning); 92 | } 93 | return loadedWarnings; 94 | } 95 | 96 | private static void testForWarning(String gav, Map> warnings) { 97 | String[] gavParts = gav.split(":"); 98 | String pluginId = gavParts[1]; 99 | String version = gavParts[2]; 100 | if (warnings.containsKey(pluginId)) { 101 | for (Warning warning : warnings.get(pluginId)) { 102 | Map versions = warning.versions; 103 | for (Pattern p : versions.keySet()) { 104 | if (p.matcher(version).matches()) { 105 | versions.replace(p, true); 106 | // written to target/surefire-reports/io.jenkins.update_center.WarningsTest-output.txt 107 | System.out.println("Warning " + warning.id + " matches " + gav); 108 | } else { 109 | System.out.println("Warning " + warning.id + " does NOT match " + gav); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | @Test 117 | public void testWarningsAgainstReleaseHistory() throws IOException { 118 | Map> warnings = loadPluginWarnings(); 119 | 120 | JSONObject json; 121 | try (final HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build()) { 122 | final HttpRequest request = HttpRequest.newBuilder() 123 | .uri(URI.create("https://updates.jenkins.io/release-history.json")) 124 | .GET() 125 | .build(); 126 | final String body = client.send(request, HttpResponse.BodyHandlers.ofString()).body(); 127 | json = JSONObject.fromObject(body); 128 | } catch (InterruptedException e) { 129 | throw new IOException(e); 130 | } 131 | 132 | JSONArray dates = json.getJSONArray("releaseHistory"); 133 | 134 | for (int dateIndex = 0; dateIndex < dates.size(); dateIndex++) { 135 | JSONObject date = dates.getJSONObject(dateIndex); 136 | 137 | JSONArray releases = date.getJSONArray("releases"); 138 | for (int releaseIndex = 0; releaseIndex < releases.size(); releaseIndex++) { 139 | JSONObject release = releases.getJSONObject(releaseIndex); 140 | 141 | try { 142 | String gav = release.getString("gav"); 143 | testForWarning(gav, warnings); 144 | } catch (JSONException ex) { 145 | // TODO wtf? 146 | } 147 | } 148 | } 149 | 150 | // TODO figure out how to deal with blacklisted plugins 151 | // for (Map.Entry warningEntry : warnings.entrySet()) { 152 | // Warning warning = warningEntry.getValue(); 153 | // for (Map.Entry patternBooleanEntry : warning.versions.entrySet()) { 154 | // if (!patternBooleanEntry.getValue()) { 155 | // Assert.fail("Pattern " + patternBooleanEntry.getKey().toString() + " did not match any release"); 156 | // } 157 | // } 158 | // } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/resources/modern.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFrzCCA5egAwIBAgIUTTROuI9hnRw1B+dRBsEr7Uv7VaYwDQYJKoZIhvcNAQEL 3 | BQAwZzETMBEGA1UECgwKU2lnbmVyVGVzdDETMBEGA1UECwwKU2lnbmVyVGVzdDET 4 | MBEGA1UEAwwKU2lnbmVyVGVzdDEmMCQGCSqGSIb3DQEJARYXZXhhbXBsZUBleGFt 5 | cGxlLmludmFsaWQwHhcNMjQxMDIyMjAxNDI0WhcNMzQxMDIwMjAxNDI0WjBnMRMw 6 | EQYDVQQKDApTaWduZXJUZXN0MRMwEQYDVQQLDApTaWduZXJUZXN0MRMwEQYDVQQD 7 | DApTaWduZXJUZXN0MSYwJAYJKoZIhvcNAQkBFhdleGFtcGxlQGV4YW1wbGUuaW52 8 | YWxpZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK/ldRUn1LAFAYqc 9 | JxB6MOwvTCEoUMfQvT4EFgVRMhVCtriDHWv6Gyli6C2NKvQj3/+SbLCbCE6VzjR8 10 | x4s7afuz/7tR7rX7EXUX9MgB1Tq4apvUKrMD1qWK4bJk7m0naw+xCN6PryiHXd54 11 | N9E3kt/7O/ei5A3yhL9wWvT5Vbe+IlmScD/UEVPCtmxju8oIJQkXgE8NXnkvn6Bc 12 | iUwaCuSffRuedFqOWAPgF4hNkowg8N3JjKtu3R9S5lLcaGyFA1OeN7flHZB429o2 13 | vDLdD4qlRkE+QaweXI2zPnviCdWOA0E/qn6TdrLrHqMBDVHy2FwrWW3Z3NmqmquE 14 | XzpcmoF3/gEkHX3sAzprP5JLCv87AbTKYIFTXdKsjTnuP+fVAWPfkFOVcUHoWO3E 15 | x79AWGUCPPiQfmiWHJsQlOYSyVqt8IhYh61hPXIOgtnCzw/HwvrDG9N3/XXyJS/b 16 | 9uOIOAli0hamwgJMIhLYwGEPemb6nhNiBDovWMLbZozxxNVFfCjSj7jAVlVHu4cj 17 | M7korupX7GtleMt8csGiatCD7IbH8MaS2szOLtBrlmLVmrkShmtnu3SvMw17XGRW 18 | 0ZG026OU2kebg8ZZJfd/xZdJzpqXubxXvEJsiR1MIEQS3yPAFFn30KSFg13tr6ZP 19 | rPzL52LPCxNy3pMcCwwdwt6BUlXjAgMBAAGjUzBRMB0GA1UdDgQWBBS7Zj7qGtVj 20 | pfbY/ggAxHXmEORDmDAfBgNVHSMEGDAWgBS7Zj7qGtVjpfbY/ggAxHXmEORDmDAP 21 | BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBZGx4SWxuxV33QlJXT 22 | 7mEAEueNJKr1B8psDYxJKHZGDlVnADT8zftyr35vQzs2SBaWtXU+4hUf/BMgkWxX 23 | Fh6lZjhU+N5mTd5eVBB0YfYb73FwaJc9jbCTXsy4eDLpw+W7LUzw7twK7LKz8XIz 24 | m6nHIDqfj6ts+QJ2sGa+dtcz1DChM3DOu4iD4Z8/KFYOhxS9JlaEzMqwbXRPmewm 25 | ONHb0z5Xw57Kf0VrA/FxFg6DJHoJVENnekot3wPML/mhGxjfHcWBEcgVKYKtQvxm 26 | KkClanrecA6Wdjw/5M4uaAQtCh4/tLOTg7msKrQ6PHQWrqkwTUCszqRfjjC4vGeT 27 | TO3h24HX7ty/j/72Idwk9DeipOVgQg8/Ld5O1vf5WfudnH7qy6bNDRoYEwSbqpAy 28 | IhZxt9nEiYievlvA9lB/cD/owYU27m1ON9XmbTHHckTQZ4X8FNXfsmjtD6auw7x2 29 | kVbwDIqwOQVxWWPlS0Hto9J3ArVyuH1xXLMq7eZTZDWWuJ3BwnRPjK2OQA4NEqRs 30 | EEJInIi8wPRQLCW78SSJBCsgav5G8jIUl7JFrrn5DOPAysTf5KhC4CNrkZB39be/ 31 | fRo8BjLbmB9Xo6jmqrhZNlh1yJVmrx9/T7FaQnjK1jgpnVzyhlqPHhmxZH/ROSXu 32 | BvsbQAUNn0tnXAeAYyOhM0/Fgg== 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /src/test/resources/modern.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | 6y3uXEUymPA0CSxXU7B7EY/M9RBtrQ== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /src/test/resources/traditional.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | B/Rl5ajepb8AoJQDiFkhxtA/bQ== 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /src/test/resources/traditional.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | --------------------------------------------------------------------------------