import URL from 'url' ; import is from '@sindresorhus/is' ; import parse from 'github-url-from-git' ; import { DateTime } from 'luxon' ; import * as hostRules from '../util/host-rules' ; import { regEx } from '../util/regex' ; import { validateUrl } from '../util/url' ; import type { ReleaseResult } from './types' ; const manualChangelogUrls = { npm : { 'babel-preset-react-app' : 'https://github.com/facebook/create-react-app/releases' , firebase : 'https://firebase.google.com/support/release-notes/js' , 'flow-bin' : 'https://github.com/facebook/flow/blob/master/Changelog.md' , gatsby : 'https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/CHANGELOG.md' , 'react-native' : 'https://github.com/react-native-community/react-native-releases/blob/master/CHANGELOG.md' , sharp : 'https://github.com/lovell/sharp/blob/master/docs/changelog.md' , 'tailwindcss-classnames' : 'https://github.com/muhammadsammy/tailwindcss-classnames/blob/master/CHANGELOG.md' , 'zone.js' : 'https://github.com/angular/angular/blob/master/packages/zone.js/CHANGELOG.md' , } , pypi : { alembic : 'https://alembic.sqlalchemy.org/en/latest/changelog.html' , beautifulsoup4 : 'https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG' , django : 'https://github.com/django/django/tree/master/docs/releases' , djangorestframework : 'https://www.django-rest-framework.org/community/release-notes/' , flake8 : 'http://flake8.pycqa.org/en/latest/release-notes/index.html' , 'django-storages' : 'https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst' , hypothesis : 'https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/docs/changes.rst' , lxml : 'https://git.launchpad.net/lxml/plain/CHANGES.txt' , mypy : 'https://mypy-lang.blogspot.com/' , phonenumbers : 'https://github.com/daviddrysdale/python-phonenumbers/blob/dev/python/HISTORY.md' , psycopg2 : 'http://initd.org/psycopg/articles/tag/release/' , 'psycopg2-binary' : 'http://initd.org/psycopg/articles/tag/release/' , pycountry : 'https://github.com/flyingcircusio/pycountry/blob/master/HISTORY.txt' , 'django-debug-toolbar' : 'https://django-debug-toolbar.readthedocs.io/en/latest/changes.html' , 'firebase-admin' : 'https://firebase.google.com/support/release-notes/admin/python' , requests : 'https://github.com/psf/requests/blob/master/HISTORY.md' , sqlalchemy : 'https://docs.sqlalchemy.org/en/latest/changelog/' , uwsgi : 'https://uwsgi-docs.readthedocs.io/en/latest/#release-notes' , wagtail : 'https://github.com/wagtail/wagtail/tree/master/docs/releases' , } , docker : { 'gitlab/gitlab-ce' : 'https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/CHANGELOG.md' , 'gitlab/gitlab-runner' : 'https://gitlab.com/gitlab-org/gitlab-runner/-/blob/master/CHANGELOG.md' , 'google/cloud-sdk' : 'https://cloud.google.com/sdk/docs/release-notes' , neo4j : 'https://neo4j.com/release-notes/' , } , } ; const manualSourceUrls = { orb : { 'cypress-io/cypress' : 'https://github.com/cypress-io/circleci-orb' , 'hutson/library-release-workflows' : 'https://github.com/hyper-expanse/library-release-workflows' , } , docker : { 'amd64/traefik' : 'https://github.com/containous/traefik' , 'coredns/coredns' : 'https://github.com/coredns/coredns' , 'docker/compose' : 'https://github.com/docker/compose' , 'drone/drone' : 'https://github.com/drone/drone' , 'drone/drone-runner-docker' : 'https://github.com/drone-runners/drone-runner-docker' , 'drone/drone-runner-kube' : 'https://github.com/drone-runners/drone-runner-kube' , 'drone/drone-runner-ssh' : 'https://github.com/drone-runners/drone-runner-ssh' , 'gcr.io/kaniko-project/executor' : 'https://github.com/GoogleContainerTools/kaniko' , 'gitlab/gitlab-ce' : 'https://gitlab.com/gitlab-org/gitlab-foss' , 'gitlab/gitlab-runner' : 'https://gitlab.com/gitlab-org/gitlab-runner' , 'gitea/gitea' : 'https://github.com/go-gitea/gitea' , 'hashicorp/terraform' : 'https://github.com/hashicorp/terraform' , node : 'https://github.com/nodejs/node' , traefik : 'https://github.com/containous/traefik' , } , kubernetes : { node : 'https://github.com/nodejs/node' , } , npm : { node : 'https://github.com/nodejs/node' , } , nvm : { node : 'https://github.com/nodejs/node' , } , pypi : { mkdocs : 'https://github.com/mkdocs/mkdocs' , 'mkdocs-material' : 'https://github.com/squidfunk/mkdocs-material' , mypy : 'https://github.com/python/mypy' , } , } ; const githubPages = regEx ( '^https://([^.]+).github.com/([^/]+)$' ) ; const gitPrefix = regEx ( '^git:/?/?' ) ; export function massageGithubUrl ( url ) { let massagedUrl = url ; if ( url . startsWith ( 'git@' ) ) { massagedUrl = url . replace ( ':' , '/' ) . replace ( 'git@' , 'https://' ) ; } return massagedUrl . replace ( 'http:' , 'https:' ) . replace ( 'http+git:' , 'https:' ) . replace ( 'https+git:' , 'https:' ) . replace ( 'ssh://git@' , 'https://' ) . replace ( gitPrefix , 'https://' ) . replace ( githubPages , 'https://github.com/$1/$2' ) . replace ( 'www.github.com' , 'github.com' ) . split ( '/' ) . slice ( 0 , 5 ) . join ( '/' ) ; } function massageGitlabUrl ( url ) { return url . replace ( 'http:' , 'https:' ) . replace ( regEx ( /^git:\/?\/?/ ) , 'https://' ) . replace ( regEx ( /\/tree\/.*$/i ) , '' ) . replace ( regEx ( /\/$/i ) , '' ) . replace ( '.git' , '' ) ; } export function normalizeDate ( input ) { if ( typeof input === 'number' && ! Number . isNaN ( input ) && input > 0 && input <= Date . now ( ) + 24 * 60 * 60 * 1000 ) { return new Date ( input ) . toISOString ( ) ; } if ( typeof input === 'string' ) { const luxonDate = DateTime . fromISO ( input , { zone : 'UTC' } ) ; if ( luxonDate . isValid ) { return luxonDate . toISO ( ) ; } return normalizeDate ( Date . parse ( input ) ) ; } if ( input instanceof Date ) { return input . toISOString ( ) ; } return null ; } function massageTimestamps ( dep ) { for ( const release of dep . releases || [ ] ) { let { releaseTimestamp } = release ; delete release . releaseTimestamp ; releaseTimestamp = normalizeDate ( releaseTimestamp ) ; if ( releaseTimestamp ) { release . releaseTimestamp = releaseTimestamp ; } } } export function addMetaData ( dep , datasource , lookupName ) { massageTimestamps ( dep ) ; const lookupNameLowercase = lookupName . toLowerCase ( ) ; const manualChangelogUrl = manualChangelogUrls [ datasource ] ?. [ lookupNameLowercase ] ; if ( manualChangelogUrl ) { dep . changelogUrl = manualChangelogUrl ; } const manualSourceUrl = manualSourceUrls [ datasource ] ?. [ lookupNameLowercase ] ; if ( manualSourceUrl ) { dep . sourceUrl = manualSourceUrl ; } if ( dep . changelogUrl ?. includes ( 'github.com' ) && ! dep . sourceUrl ) { dep . sourceUrl = dep . changelogUrl ; } if ( dep . homepage ?. includes ( 'github.com' ) ) { if ( ! dep . sourceUrl ) { dep . sourceUrl = dep . homepage ; } delete dep . homepage ; } const extraBaseUrls = [ ] ; hostRules . hosts ( { hostType : 'github' } ) . forEach ( ( host ) => { extraBaseUrls . push ( host , `gist.${ host }` ) ; } ) ; extraBaseUrls . push ( 'gitlab.com' ) ; if ( dep . sourceUrl ) { const parsedUrl = URL . parse ( dep . sourceUrl ) ; if ( parsedUrl ?. hostname ) { let massagedUrl ; if ( parsedUrl . hostname . includes ( 'gitlab' ) ) { massagedUrl = massageGitlabUrl ( dep . sourceUrl ) ; } else { massagedUrl = massageGithubUrl ( dep . sourceUrl ) ; } dep . sourceUrl = parse ( massagedUrl , { extraBaseUrls , } ) || dep . sourceUrl ; } else { delete dep . sourceUrl ; } } const urlKeys = [ 'homepage' , 'sourceUrl' , 'changelogUrl' , 'dependencyUrl' , ] ; for ( const urlKey of urlKeys ) { const urlVal = dep [ urlKey ] ; if ( is . string ( urlVal ) && validateUrl ( urlVal . trim ( ) ) ) { dep [ urlKey ] = urlVal . trim ( ) as never ; } else { delete dep [ urlKey ] ; } } }