diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..f859b1278 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-label: 'stale' + stale-pr-label: 'stale' + stale-issue-message: 'Issue is now considered stale. If you want to keep it open, please comment :+1:' + stale-pr-message: 'PR is now considered stale. If you want to keep it open, please comment :+1:' + close-issue-message: 'Issue was closed due to inactivity.' + close-pr-message: 'PR was closed due to inactivity.' + days-before-stale: 182 + days-before-close: 365 + operations-per-run: 50 diff --git a/.gitignore b/.gitignore index 08462849d..fbb8167e7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ ._* .vscode certbot-help.txt +test/node_modules +*/node_modules +docker/dev/dnsrouter-config.json.tmp +docker/dev/resolv.conf diff --git a/.version b/.version index 2e12e4762..e4643748f 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.9.14 +2.12.6 diff --git a/Jenkinsfile b/Jenkinsfile index 1b7446924..af913c2e0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,9 @@ +import groovy.transform.Field + +@Field +def shOutput = "" +def buildxPushTags = "" + pipeline { agent { label 'docker-multiarch' @@ -8,14 +14,12 @@ pipeline { ansiColor('xterm') } environment { - IMAGE = "nginx-proxy-manager" + IMAGE = 'nginx-proxy-manager' BUILD_VERSION = getVersion() - MAJOR_VERSION = "2" - BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}" - COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" - COMPOSE_FILE = 'docker/docker-compose.ci.yml' + MAJOR_VERSION = '2' + BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}" + BUILDX_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_INTERACTIVE_NO_CLI = 1 - BUILDX_NAME = "${COMPOSE_PROJECT_NAME}" } stages { stage('Environment') { @@ -26,7 +30,7 @@ pipeline { } steps { script { - env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" + buildxPushTags = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" } } } @@ -39,7 +43,7 @@ pipeline { steps { script { // Defaults to the Branch name, which is applies to all branches AND pr's - env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" + buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}" } } } @@ -52,108 +56,153 @@ pipeline { sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md' } } + stage('Docker Login') { + steps { + withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { + sh 'docker login -u "${duser}" -p "${dpass}"' + } + } + } } } - stage('Frontend') { - steps { - sh './scripts/frontend-build' + stage('Builds') { + parallel { + stage('Project') { + steps { + script { + // Frontend and Backend + def shStatusCode = sh(label: 'Checking and Building', returnStatus: true, script: ''' + set -e + ./scripts/ci/frontend-build > ${WORKSPACE}/tmp-sh-build 2>&1 + ./scripts/ci/test-and-build > ${WORKSPACE}/tmp-sh-build 2>&1 + ''') + shOutput = readFile "${env.WORKSPACE}/tmp-sh-build" + if (shStatusCode != 0) { + error "${shOutput}" + } + } + } + post { + always { + sh 'rm -f ${WORKSPACE}/tmp-sh-build' + } + failure { + npmGithubPrComment("CI Error:\n\n```\n${shOutput}\n```", true) + } + } + } + stage('Docs') { + steps { + dir(path: 'docs') { + sh 'yarn install' + sh 'yarn build' + } + } + } } } - stage('Backend') { - steps { - echo 'Checking Syntax ...' - sh 'docker pull nginxproxymanager/nginx-full:certbot-node' - // See: https://github.com/yarnpkg/yarn/issues/3254 - sh '''docker run --rm \\ - -v "$(pwd)/backend:/app" \\ - -v "$(pwd)/global:/app/global" \\ - -w /app \\ - nginxproxymanager/nginx-full:certbot-node \\ - sh -c "yarn install && yarn eslint . && rm -rf node_modules" - ''' - - echo 'Docker Build ...' - sh '''docker build --pull --no-cache --squash --compress \\ - -t "${IMAGE}:ci-${BUILD_NUMBER}" \\ - -f docker/Dockerfile \\ - --build-arg TARGETPLATFORM=linux/amd64 \\ - --build-arg BUILDPLATFORM=linux/amd64 \\ - --build-arg BUILD_VERSION="${BUILD_VERSION}" \\ - --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\ - --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\ - . - ''' + stage('Test Sqlite') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_sqlite" + COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.sqlite.yml' + } + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } } - } - stage('Integration Tests Sqlite') { steps { - // Bring up a stack - sh 'docker-compose up -d fullstack-sqlite' - sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120' - - // Run tests - sh 'rm -rf test/results' - sh 'docker-compose up cypress-sqlite' - // Get results - sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/' + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' } post { always { // Dumps to analyze later - sh 'mkdir -p debug' - sh 'docker-compose logs fullstack-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz' - sh 'docker-compose logs db | gzip > debug/docker_db.log.gz' - // Cypress videos and screenshot artifacts + sh 'mkdir -p debug/sqlite' + sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/sqlite/docker_fullstack.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q stepca) > debug/sqlite/docker_stepca.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns) > debug/sqlite/docker_pdns.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/sqlite/docker_pdns-db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/sqlite/docker_dnsrouter.log 2>&1' + junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' + } + unstable { dir(path: 'test/results') { - archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' + archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') } - junit 'test/results/junit/*' } } } - stage('Integration Tests Mysql') { + stage('Test Mysql') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_mysql" + COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.mysql.yml' + } + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } steps { - // Bring up a stack - sh 'docker-compose up -d fullstack-mysql' - sh './scripts/wait-healthy $(docker-compose ps -q fullstack-mysql) 120' - - // Run tests - sh 'rm -rf test/results' - sh 'docker-compose up cypress-mysql' - // Get results - sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/' + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' } post { always { // Dumps to analyze later - sh 'mkdir -p debug' - sh 'docker-compose logs fullstack-mysql | gzip > debug/docker_fullstack_mysql.log.gz' - sh 'docker-compose logs db | gzip > debug/docker_db.log.gz' - // Cypress videos and screenshot artifacts + sh 'mkdir -p debug/mysql' + sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/mysql/docker_fullstack.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q stepca) > debug/mysql/docker_stepca.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns) > debug/mysql/docker_pdns.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/mysql/docker_pdns-db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/mysql/docker_dnsrouter.log 2>&1' + junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' + } + unstable { dir(path: 'test/results') { - archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' + archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') } - junit 'test/results/junit/*' } } } - stage('Docs') { + stage('Test Postgres') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres" + COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml' + } when { not { equals expected: 'UNSTABLE', actual: currentBuild.result } } steps { - dir(path: 'docs') { - sh 'yarn install' - sh 'yarn build' - } + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' + } + post { + always { + // Dumps to analyze later + sh 'mkdir -p debug/postgres' + sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1' - dir(path: 'docs/.vuepress/dist') { - sh 'tar -czf ../../docs.tgz *' + junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' + } + unstable { + dir(path: 'test/results') { + archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') + } } - - archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false) } } stage('MultiArch Build') { @@ -163,81 +212,64 @@ pipeline { } } steps { - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - // Docker Login - sh "docker login -u '${duser}' -p '${dpass}'" - // Buildx with push from cache - sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" - } + sh "./scripts/buildx --push ${buildxPushTags}" } } - stage('Docs Deploy') { - when { - allOf { - branch 'master' - not { - equals expected: 'UNSTABLE', actual: currentBuild.result + stage('Docs / Comment') { + parallel { + stage('Docs Job') { + when { + allOf { + branch pattern: "^(develop|master)\$", comparator: "REGEXP" + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + } + steps { + build wait: false, job: 'nginx-proxy-manager-docs', parameters: [string(name: 'docs_branch', value: "$BRANCH_NAME")] } } - } - steps { - withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { - sh """docker run --rm \\ - --name \${COMPOSE_PROJECT_NAME}-docs-upload \\ - -e S3_BUCKET=jc21-npm-site \\ - -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ - -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ - -v \$(pwd):/app \\ - -w /app \\ - jc21/ci-tools \\ - scripts/docs-upload /app/docs/.vuepress/dist/ - """ + stage('PR Comment') { + when { + allOf { + changeRequest() + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + } + steps { + script { + npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev): +``` +nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER} +``` - sh """docker run --rm \\ - --name \${COMPOSE_PROJECT_NAME}-docs-invalidate \\ - -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ - -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ - jc21/ci-tools \\ - aws cloudfront create-invalidation --distribution-id EN1G6DEWZUTDT --paths '/*' - """ - } - } - } - stage('PR Comment') { - when { - allOf { - changeRequest() - not { - equals expected: 'UNSTABLE', actual: currentBuild.result +> [!NOTE] +> Ensure you backup your NPM instance before testing this image! Especially if there are database changes. +> This is a different docker image namespace than the official image. + +> [!WARNING] +> Changes and additions to DNS Providers require verification by at least 2 members of the community! +""", true) + } } } } - steps { - script { - def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") - } - } } } post { always { - sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30' sh 'echo Reverting ownership' - sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data' - } - success { - juxtapose event: 'success' - sh 'figlet "SUCCESS"' + sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data' + printResult(true) } failure { - archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) - juxtapose event: 'failure' - sh 'figlet "FAILURE"' + archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) } unstable { - archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) - juxtapose event: 'unstable' - sh 'figlet "UNSTABLE"' + archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) } } } diff --git a/README.md b/README.md index 64c7c7ab6..2116a55ae 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@
This project comes as a pre-built docker image that enables you to easily forward to your websites @@ -28,7 +19,7 @@ running at home or otherwise, including free SSL, without having to know too muc ## Project Goal -I created this project to fill a personal need to provide users with a easy way to accomplish reverse +I created this project to fill a personal need to provide users with an easy way to accomplish reverse proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed. While there might be advanced options they are optional and the project should be as simple as possible so that the barrier for entry here is low. @@ -65,10 +56,9 @@ I won't go in to too much detail here but here are the basics for someone new to 2. Create a docker-compose.yml file similar to this: ```yml -version: '3' services: app: - image: 'jc21/nginx-proxy-manager:latest' + image: 'docker.io/jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: - '80:80' @@ -79,10 +69,16 @@ services: - ./letsencrypt:/etc/letsencrypt ``` +This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more. + 3. Bring up your stack by running ```bash docker-compose up -d + +# If using docker-compose-plugin +docker compose up -d + ``` 4. Log in to the Admin UI @@ -101,417 +97,24 @@ Password: changeme Immediately after logging in with this default user you will be asked to modify your details and change your password. -## Contributors - -Special thanks to the following contributors: - - - -- Expose web services on your network · - Free SSL with Let's Encrypt · - Designed with security in mind · - Perfect for home networks -
-Expose your private network Web services and get connected anywhere.
-Based on Tabler, the interface is a pleasure to use. Configuring a server has never been so fun.
-Built in Let’s Encrypt support allows you to secure your Web services at no cost to you. The certificates even renew themselves!
-Built as a Docker Image, Nginx Proxy Manager only requires a database.
-Configure other users to either view or manage their own hosts. Full access permissions are available.
-