diff --git a/.all-contributorsrc b/.all-contributorsrc index 223affe2..bd0bec05 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,5 +1,5 @@ { - "projectName": "thetvapp-docker", + "projectName": "tvapp2", "projectOwner": "Aetherinox", "repoType": "github", "repoHost": "https://github.com", @@ -12,15 +12,22 @@ "login": "Aetherinox", "name": "Aetherinox", "avatar_url": "https://avatars.githubusercontent.com/u/118329232?v=4", - "profile": "https://gitlab.com/Aetherinox", - "contributions": ["code", "projectManagement"] + "profile": "https://github.com/Aetherinox", + "contributions": ["code"] }, { - "login": "dtankdempse", - "name": "dtankdempse", - "avatar_url": "https://avatars.githubusercontent.com/u/175421607?v=4", - "profile": "https://gitlab.com/dtankdempse", - "contributions": ["tools"] + "login": "iFlip721", + "name": "iFlip721", + "avatar_url": "https://avatars.githubusercontent.com/u/28721588?v=4", + "profile": "https://github.com/iFlip721", + "contributions": ["code"] + }, + { + "login": "Optx", + "name": "Optx", + "avatar_url": "https://avatars.githubusercontent.com/u/32874812?v=4", + "profile": "https://github.com/Nvmdfth", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index cdb1a82f..00000000 --- a/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -.git -.gitignore -.github -.gitattributes -READMETEMPLATE.md -README.md diff --git a/.editorconfig b/.editorconfig index b58e38d6..874d8e4a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,20 @@ -# http://editorconfig.org +# # +# @file .editorconfig +# @author Aetherinox https://github.com/Aetherinox +# https://git.binaryninja.net/Aetherinox +# @ref http://editorconfig.org +# # + +# # +# Is top-most EditorConfig file +# # -# is top-most EditorConfig file root = true -# All Files +# # +# All Files +# # + [*] indent_style = space indent_size = 4 @@ -12,11 +23,17 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -# Markdown Files +# # +# Markdown Files +# # + [*.md] trim_trailing_whitespace = false -# Other +# # +# Other +# # + [{*.nsh,*.yml,*.yaml,*.json}] indent_style = space indent_size = 2 diff --git a/.gitattributes b/.gitattributes index bdb0cabc..84d37844 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,17 +1,32 @@ -# Auto detect text files and perform LF normalization +# # +# @file .gitattritutes +# @author Aetherinox https://github.com/Aetherinox +# https://git.binaryninja.net/Aetherinox +# # + +# # +# Auto detect text files and set LF +# # + * text=auto -# Custom for Visual Studio +# # +# Visual Studio +# # + *.cs diff=csharp -# Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain +# # +# msysgit +# # + +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 3f50c735..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,10 +0,0 @@ -custom: ["https://buymeacoffee.com/aetherinox"] -github: # [repo-name, aetherinox] -patreon: # Replace with a single Patreon username -open_collective: # name -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml deleted file mode 100644 index c75b6e56..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: "🐛 Bug Report" -description: Found something you weren't expecting? Report it here! -title: "🐛 Bug: " -labels: [ - "Type ◦ Bug" -] -body: - - type: markdown - attributes: - value: | - 1. Please speak `English`. - 2. Make sure you are using the latest version and take a moment to check that your issue hasn't been reported before. - 3. It's really important to provide pertinent details and logs, - incomplete details will be handled as an invalid report. - - <br /> - - - type: textarea - id: description - attributes: - label: Description - description: | - Please provide a description of your issue here. - validations: - required: true - - - type: textarea - id: steps-reproduce - attributes: - label: Steps To Reproduce - description: | - Describe the steps that need taken by the developer to get the error / issue you're experiencing. - value: | - - - - - - - - - validations: - required: true - - - type: input - id: version-thetvapp - attributes: - label: "Version - Tag" - description: | - Version / tag you are pulling for `thetvapp` - placeholder: "Ex: 1.0.0" - validations: - required: true - - - type: input - id: version-docker - attributes: - label: "Version - Docker" - description: "Version of docker you are running. Use command `docker --version`." - placeholder: "Ex: 27.2.0, build 3ab4256" - validations: - required: true - - - type: dropdown - id: image-source - attributes: - label: Docker Image Source - description: | - Select which docker image you are pulling from - options: - - "Github" - - "Dockerhub" - - "Custom Built" - validations: - required: true - - - type: dropdown - id: priority-type - attributes: - label: Priority - description: | - How critical is the issue? - Do not abuse this. Issues that completely break the utility would be classified as critical - options: - - "Low" - - "Normal" - - "High" - - "Urgent" - validations: - required: true - - - type: textarea - id: docker-compose - attributes: - label: docker-compose.yml - description: | - Copy / paste your `docker-compose.yml` file here - - - type: textarea - id: logs - attributes: - label: Logs - description: | - Paste your docker logs here. - Paste logs from inside mounted volume `/config/log/*` - - - type: textarea - id: screenshots - attributes: - label: Screenshots - description: | - Please provide screenshots of any errors or the issue you're having. - Gifs are even better. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 6edea9c0..00000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "💡 Feature Request" -description: Got a suggestion? Submit your request here. -title: "💡 Feature: <title>" -labels: [ - "Type ◦ Feature" -] -body: - - type: markdown - attributes: - value: | - 1. Please speak English. - 2. Please take a moment to check that your feature hasn't already been suggested. - 3. Be detailed but to the point. - - - type: textarea - id: description - attributes: - label: Feature Description - placeholder: | - I would like to request ... - validations: - required: true - - - type: textarea - id: screenshots - attributes: - label: Screenshots - description: | - If possible, provide screenshots. - Want a feature placed in a specific location? Mark it in a screenshot. - Want something modified? Try creating a mockup. - The more details about how it should look, the better. - Not required, but appreciated. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 65b25132..83e54a66 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,15 +7,16 @@ This text will remain hidden when you submit your pull request. For your pull request title, use the format: - [BUG]: Brief title of the bug being fixed - [FEATURE]: Brief title of the feature being added + [BUG]: Brief title of the bug being fixed + [FEATURE]: Brief title of the feature being added + [DOCS]: Brief title of the feature being added Failure to follow the above title format will result in your PR being ignored. --> # Pull Request -<small>Checkmark which topic best describes your contribution:</small> +<small>Select which topic best describes your contribution:</small> - [ ] Feature - [ ] Bug @@ -43,7 +44,7 @@ ### Before You Submit <small>Please ensure you check the following items to indicate that you've read this section and completed each task</small> -- [ ] My code follows the [Contribution Guidelines](https://github.com/Aetherinox/thetvapp-docker/blob/main/CONTRIBUTING.md) +- [ ] My code follows the [Contribution Guidelines](https://github.com/https://github.com/iFlip721/tvapp2/blob/main/CONTRIBUTING.md) - [ ] I give expressed consent for my work to be used in this repo - [ ] I have tested my work and it functions as intended - [ ] I have included documentation if the change requires such diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 692808b1..dbc614e2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ # # # MIT License # -# Copyright (c) 2025 Aetherinox +# Copyright (c) 2024-2025 Aetherinox # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/.github/labeler.yml b/.github/labeler.yml index 4085b2ce..eb79b329 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,3 +1,27 @@ +# # +# MIT License +# +# Copyright (c) 2024-2025 Aetherinox +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# # + # Number of labels to fetch (optional). Defaults to 100 numLabels: 40 # These labels will not be used even if the issue contains them (optional). @@ -42,4 +66,4 @@ custom: keywords: - '[request]' labels: - - Type ◦ Feature \ No newline at end of file + - Type ◦ Feature diff --git a/.github/workflows/deploy-clean.yml b/.github/workflows/deploy-clean.yml index 235b60dd..bf1f0df7 100644 --- a/.github/workflows/deploy-clean.yml +++ b/.github/workflows/deploy-clean.yml @@ -1,8 +1,15 @@ # # # @type github workflow # @desc cleans up the list of deployments in the environment history +# edit the 'environment:' to determine which deployment to keep clean +# - can be ran manually # @author Aetherinox # @url https://github.com/Aetherinox +# +# @secrets secrets.SELF_TOKEN_CL Github Access Token (Classic) +# secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_WORKFLOWS Discord Webbhook URL; right-click on channel, click "Integrations" +# # + # # name: "⚙️ Deploy › Clean" @@ -14,15 +21,40 @@ run-name: "⚙️ Deploy › Clean" on: workflow_dispatch: + inputs: + + # # + # Deployment Environment Name + # + # this is the name of the deployment item + # # + + DEPLOYMENT_ENV: + description: '📦 Deployment Environment' + required: true + default: 'orion' + type: string + + # # + # Delay + # + # Milliseconds to wait between cleaning up each action in history. Avoids secondary rate limit. Default: 500 + # # + + DEPLOYMENT_DELAY: + description: '🕛 Delete Delay' + required: true + default: '1000' + type: string # # # environment variables # # env: - BOT_NAME_1: AdminServ - BOT_NAME_2: AdminServX - BOT_NAME_3: EuropaServ + DEPLOYMENT_ENV: ${{ github.event.inputs.DEPLOYMENT_ENV || 'orion' }} + DEPLOYMENT_DELAY: ${{ github.event.inputs.DEPLOYMENT_DELAY || '1000' }} + BOT_NAME_1: EuropaServ BOT_NAME_DEPENDABOT: dependabot[bot] LABELS_JSON: | [ @@ -94,13 +126,126 @@ jobs: cleanup: runs-on: ubuntu-latest permissions: write-all - + steps: + + # # + # Cleanup › Set Env Variables + # # + + - name: >- + 🕛 Get Timestamp + id: task_cleanup_set_timestamp + run: | + echo "NOW=$(date +'%m-%d-%Y %H:%M:%S')" >> $GITHUB_ENV + echo "NOW_SHORT=$(date +'%m-%d-%Y')" >> $GITHUB_ENV + echo "NOW_LONG=$(date +'%m-%d-%Y %H:%M')" >> $GITHUB_ENV + echo "NOW_DOCKER_LABEL=$(date +'%Y%m%d')" >> $GITHUB_ENV + + # # + # Release › Github › Checkout › Arm64 + # # + + - name: >- + ✅ Checkout + id: task_cleanup_gh_checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # # + # Cleanup › Start + # # + - name: >- ⚙️ Deployments › Clean + id: task_cleanup_start uses: Aetherinox/delete-deploy-env-action@v3 with: token: ${{ secrets.SELF_TOKEN_CL }} - environment: orion + environment: '${{ env.DEPLOYMENT_ENV }}' onlyRemoveDeployments: true - delay: "1000" \ No newline at end of file + delay: "${{ env.DEPLOYMENT_DELAY }}" + + # # + # Cleanup › Get Weekly Commits + # # + + - name: >- + 🕛 Get Weekly Commit List + id: task_cleanup_set_weekly_commit_list + run: | + echo 'WEEKLY_COMMITS<<EOF' >> $GITHUB_ENV + git log --format="[\`%h\`](${{ github.server_url }}/${{ github.repository }}/commit/%H) %s - %an" --since=7.days >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + + # # + # Cleanup › Notify Github › Success + # # + + - name: >- + 🔔 Send Discord Webhook Message (Success) + id: task_cleanup_notify_discord_success + uses: tsickert/discord-webhook@v6.0.0 + if: success() + with: + username: 'Io' + avatar-url: 'https://i.imgur.com/8BVDkla.jpg' + webhook-url: ${{ secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_WORKFLOWS }} + embed-title: "**Deployment Cleanup Workflow Ran**" + embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + embed-thumbnail-url: 'https://i.imgur.com/zDIzE8T.jpg' + embed-description: | + ## 📦 Deployment Cleanup ${{ job.status == 'success' && '✅' || '❌' }} + + A **successful** deployment cleanup was triggered on your repository. The history for this environment has been wiped + and will no longer list previous deployments you've made. + + - Environment: `${{ env.DEPLOYMENT_ENV }}` + - Cleanup Delay: `${{ env.DEPLOYMENT_DELAY }}` + - Workflow: `${{ github.workflow }} (#${{github.run_number}})` + - Triggered By: ${{ github.actor }} + - Status: `${{ job.status == 'success' && '✅ Successful' || '❌ Failed' }}` + + embed-color: ${{ job.status == 'success' && '5763719' || '15418782' }} + embed-footer-text: "Completed at ${{ env.NOW }} UTC" + embed-timestamp: "${{ env.NOW_LONG }}" + embed-author-name: "${{steps.embed.outputs.EMBED_AUTHOR_NAME}}" + embed-author-url: "${{ github.event.release.author.html_url }}" + embed-author-icon-url: "${{ github.event.release.author.avatar_url }}" + + # # + # Cleanup › Notify Github › Failure + # # + + - name: >- + 🔔 Send Discord Webhook Message (Failure) + id: task_cleanup_notify_discord_failure + uses: tsickert/discord-webhook@v6.0.0 + if: failure() + with: + username: 'Io' + avatar-url: 'https://i.imgur.com/8BVDkla.jpg' + webhook-url: ${{ secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_WORKFLOWS }} + embed-title: "**Deployment Cleanup Workflow Ran**" + embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + embed-thumbnail-url: 'https://i.imgur.com/zDIzE8T.jpg' + embed-description: | + ## 📦 Deployment Cleanup ${{ job.status == 'success' && '✅' || '❌' }} + + A **failed** deployment cleanup was triggered on your repository. Since the action failed; no entries of your repo's + deployment history have been removed. + + - Environment: `${{ env.DEPLOYMENT_ENV }}` + - Cleanup Delay: `${{ env.DEPLOYMENT_DELAY }}` + - Workflow: `${{ github.workflow }} (#${{github.run_number}})` + - Triggered By: ${{ github.actor }} + - Status: `${{ job.status == 'success' && '✅ Successful' || '❌ Failed' }}` + + embed-color: ${{ env.STATUS == 'success' && '5763719' || '15418782' }} + embed-footer-text: "Completed at ${{ env.NOW }} UTC" + embed-timestamp: "${{ env.NOW_LONG }}" + embed-author-name: "${{steps.embed.outputs.EMBED_AUTHOR_NAME}}" + embed-author-url: "${{ github.event.release.author.html_url }}" + embed-author-icon-url: "${{ github.event.release.author.avatar_url }}" + diff --git a/.github/workflows/deploy-docker-dockerhub.yml b/.github/workflows/deploy-docker-dockerhub.yml new file mode 100644 index 00000000..4fbdce4e --- /dev/null +++ b/.github/workflows/deploy-docker-dockerhub.yml @@ -0,0 +1,523 @@ +# # +# @type github workflow +# @author Aetherinox +# @url https://github.com/Aetherinox +# @usage deploys docker container to Dockerhub +# @secrets secrets.ADMINSERV_GPG_KEY_ASC gpg private key (armored) | BEGIN PGP PRIVATE KEY BLOCK +# secrets.ADMINSERV_GPG_PASSPHRASE gpg private key passphrase +# secrets.IMAGE_DOCKERHUB_TOKEN hub.docker.com access token +# # + +name: "📦 Deploy › Docker › Dockerhub" +run-name: "📦 Deploy › Docker › Dockerhub" + +# # +# Triggers +# # + +on: + + # # + # Trigger › Workflow Dispatch + # + # If any values are not provided, will use fallback env variable + # # + + workflow_dispatch: + inputs: + + # # + # Image Name + # + # used in github image path + # ${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + # # + + IMAGE_NAME: + description: '📦 Image Name' + required: true + default: 'keeweb' + type: string + + # # + # Image Author + # + # used in github image path + # ${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + # # + + IMAGE_AUTHOR: + description: '🪪 Image Author' + required: true + default: 'antelle' + type: string + + # # + # Image Version + # + # used to create new release tag, and add version to docker image name + # # + + IMAGE_VERSION: + description: '🏷️ Image Version' + required: true + default: '1.19.0' + type: string + + # # + # Image Dockerhub username + # + # this is the user to sign into Dockerhub as. + # # + + IMAGE_DOCKERHUB_USERNAME: + description: '🪪 Dockerhub Username' + required: true + default: 'antelle' + type: string + + # # + # true no changes to the repo will be made + # false workflow will behave normally, and push any changes detected to the files + # # + + DRY_RUN: + description: '🐛 Dry Run (Debug)' + required: true + default: false + type: boolean + + # # + # true released version will be marked as a development build and will have the v1.x.x-development tag instead of -latest + # false release version will be marked with -latest docker tag + # # + + DEV_RELEASE: + description: '🧪 Development Release' + required: true + default: false + type: boolean + + # # + # Trigger › Push + # # + + push: + tags: + - '*' + +# # +# Environment Vars +# # + +env: + IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'keeweb' }} + IMAGE_AUTHOR: ${{ github.event.inputs.IMAGE_AUTHOR || 'antelle' }} + IMAGE_VERSION: ${{ github.event.inputs.IMAGE_VERSION || '1.19.0' }} + IMAGE_DOCKERHUB_USERNAME: ${{ github.event.inputs.IMAGE_DOCKERHUB_USERNAME || 'antelle' }} + BOT_NAME_1: EuropaServ + BOT_NAME_DEPENDABOT: dependabot[bot] + +# # +# Jobs +# +# The way pushed docker containers on Dockerhub work, the most recent image built goes at the top. +# We will use the order below which builds the :latest image last so that it appears at the very +# top of the packages page. +# # + +jobs: + + # # + # Job › Create Tag + # # + + job-docker-release-tags-create: + name: >- + 📦 Release › Create Tag + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + attestations: write + id-token: write + steps: + + # # + # Release › Tags › Start + # # + + - name: '🏳️ Start' + id: task_release_tags_start + run: | + echo "Creating Tag" + + # # + # Release › Tags › Checkout + # # + + - name: '✅ Checkout' + id: task_release_tags_checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # # + # Release › Tags › Fix Permissions + # # + + - name: '#️⃣ Manage Permissions' + id: task_release_tags_permissions + run: | + find ./ -name 'run' -exec chmod 755 {} \; + WRONG_PERM=$(find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print) + if [ -n "${WRONG_PERM}" ]; then + echo "⚠️⚠️⚠️ Permissions are invalid ⚠️⚠️⚠️" + for i in ${WRONG_PERM}; do + echo "::error file=${i},line=1,title=Missing Executable Bit::This file needs to be set as executable!" + done + exit 1 + else + echo "✅✅✅ Executable permissions are OK ✅✅✅" + fi + + # # + # Release › Tags › Create Tag + # + # only called in dispatch mode + # # + + - uses: rickstaa/action-create-tag@v1 + id: task_release_tags_create + if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false ) + with: + tag: "${{ env.IMAGE_VERSION }}" + tag_exists_error: false + message: '${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}' + gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }} + gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }} + + # # + # Job › Docker Release › Dockerhub › Arm64 + # # + + job-docker-release-dockerhub-arm64: + name: >- + 📦 Release › Dockerhub › Arm64 + runs-on: ubuntu-latest + needs: [ job-docker-release-tags-create ] + permissions: + contents: write + packages: write + attestations: write + id-token: write + steps: + + # # + # Release › Dockerhub › Start › Arm64 + # # + + - name: '🏳️ Start' + id: task_release_dh_start + run: | + echo "Starting Dockerhub arm64" + + # # + # Release › Dockerhub › Checkout › Arm64 + # # + + - name: '✅ Checkout' + id: task_release_dh_checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # # + # Release › Dockerhub › Install Dependencies + # # + + - name: '📦 Install Dependencies' + id: task_release_dh_dependencies + run: + sudo apt-get install -qq dos2unix + + # # + # Release › Dockerhub › Execute dos2unix + # # + + - name: '🔐 Apply dos2unix' + id: task_release_dh_dos2unix + run: | + echo "⚠️⚠️⚠️ Running DOS2UNIX ⚠️⚠️⚠️" + find ./ \( -path "./.git" -o -path "./docs" -o -path "./.github" -o -path "*.png" -o -path "*.jpg" \) -prune -o -name '*' -print | xargs dos2unix -- + echo "✅✅✅ Completed DOS2UNIX ✅✅✅" + + # # + # Release › Dockerhub › Fix Permissions + # # + + - name: '#️⃣ Manage Permissions' + id: task_release_dh_permissions + run: | + find ./ -name 'run' -exec chmod 755 {} \; + WRONG_PERM=$(find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print) + if [ -n "${WRONG_PERM}" ]; then + echo "⚠️⚠️⚠️ Permissions are invalid ⚠️⚠️⚠️" + for i in ${WRONG_PERM}; do + echo "::error file=${i},line=1,title=Missing Executable Bit::This file needs to be set as executable!" + done + exit 1 + else + echo "✅✅✅ Executable permissions are OK ✅✅✅" + fi + + # # + # Release › Dockerhub › QEMU › Arm64 + # # + + - name: '⚙️ Set up QEMU' + id: task_release_dh_qemu + uses: docker/setup-qemu-action@v3 + + # # + # Release › Dockerhub › Setup BuildX › Arm64 + # # + + - name: '⚙️ Setup Buildx' + id: task_release_dh_buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + driver-opts: 'image=moby/buildkit:latest' + + # # + # Release › Dockerhub › Registry Login › Arm64 + # # + + - name: '⚙️ Login to Dockerhub' + id: task_release_dh_registry + uses: docker/login-action@v3 + with: + username: ${{ env.IMAGE_DOCKERHUB_USERNAME }} + password: ${{ secrets.IMAGE_DOCKERHUB_TOKEN }} + + # # + # Release › Dockerhub › Meta › Arm64 + # # + + - name: '🔨 Dockerhub: Meta - Arm64' + id: task_release_dh_meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + tags: | + # latest no + type=raw,value=latest,enable=false + + # dispatch add x1.x.x-arm64 + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=300,prefix=,suffix=-arm64,value=${{ env.IMAGE_VERSION }} + + # dispatch add arm64-development + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == true }},priority=300,prefix=,suffix=-development,value=arm64 + + # tag add tag-arm64 + type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=-arm64,event=tag + flavor: | + latest=false + + # # + # Release › Dockerhub › Checkpoint › Arm64 + # # + + - name: '⚠️ Checkpoint' + id: task_release_dh_checkpoint + run: | + echo "registry ............. Github" + echo "github.actor.......... ${{ github.actor }}" + echo "github.ref ........... ${{ github.ref }}" + echo "github.ref_name ...... ${{ github.ref_name }}" + echo "github.event_name .... ${{ github.event_name }}" + echo "inputs.DRY_RUN ....... ${{ inputs.DRY_RUN }}" + echo "env.AUTHOR ........... ${{ env.IMAGE_AUTHOR }}" + echo "tags ................. ${{ steps.task_release_dh_meta.outputs.tags }}" + echo "labels ............... ${{ steps.task_release_dh_meta.outputs.labels }}" + + # # + # Release › Dockerhub › Build and Push › Arm64 + # # + + - name: '📦 Build & Push (linux/arm64)' + id: task_release_dh_push + uses: docker/build-push-action@v6 + if: ( github.event_name == 'workflow_dispatch' && inputs.DRY_RUN == false ) || ( github.event_name == 'push' ) + with: + context: . + file: Dockerfile.aarch64 + platforms: linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.task_release_dh_meta.outputs.tags }} + labels: ${{ steps.task_release_dh_meta.outputs.labels }} + + # # + # Job › Docker Release › Dockerhub › Amd64 + # # + + job-docker-release-dockerhub-amd64: + name: >- + 📦 Release › Dockerhub › Amd64 + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + attestations: write + id-token: write + needs: [ job-docker-release-tags-create, job-docker-release-dockerhub-arm64 ] + steps: + + # # + # Release › Dockerhub › Start › Amd64 + # # + + - name: '🏳️ Start' + id: task_release_dh_start + run: | + echo "Starting Dockerhub docker release" + + # # + # Release › Dockerhub › Checkout › Amd64 + # # + + - name: '✅ Checkout' + id: task_release_dh_checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # # + # Release › Dockerhub › Install Dependencies + # # + + - name: '📦 Install Dependencies' + id: task_release_dh_dependencies + run: + sudo apt-get install -qq dos2unix + + # # + # Release › Dockerhub › Execute dos2unix + # # + + - name: '🔐 Apply dos2unix' + id: task_release_dh_dos2unix + run: | + find ./ \( -path "./.git" -o -path "./docs" -o -path "./.github" -o -path "*.png" -o -path "*.jpg" \) -prune -o -name '*' -print | xargs dos2unix -- + + # # + # Release › Dockerhub › Fix Permissions + # # + + - name: '#️⃣ Manage Permissions' + id: task_release_dh_permissions + run: | + find ./ -name 'run' -exec chmod 755 {} \; + WRONG_PERM=$(find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print) + if [ -n "${WRONG_PERM}" ]; then + echo "⚠️⚠️⚠️ Permissions are invalid ⚠️⚠️⚠️" + for i in ${WRONG_PERM}; do + echo "::error file=${i},line=1,title=Missing Executable Bit::This file needs to be set as executable!" + done + exit 1 + else + echo "✅✅✅ Executable permissions are OK ✅✅✅" + fi + + # # + # Release › Dockerhub › QEMU › Amd64 + # # + + - name: '⚙️ Set up QEMU' + id: task_release_dh_qemu + uses: docker/setup-qemu-action@v3 + + # # + # Release › Dockerhub › Setup BuildX › Amd64 + # # + + - name: '⚙️ Setup Buildx' + id: task_release_dh_buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + driver-opts: 'image=moby/buildkit:latest' + + # # + # Release › Dockerhub › Registry Login › Amd64 + # # + + - name: '⚙️ Login to Dockerhub' + id: task_release_dh_registry + uses: docker/login-action@v3 + with: + username: ${{ env.IMAGE_DOCKERHUB_USERNAME }} + password: ${{ secrets.IMAGE_DOCKERHUB_TOKEN }} + + # # + # Release › Dockerhub › Meta › Amd64 + # # + + - name: '🔨 Dockerhub: Meta - Amd64' + id: task_release_dh_meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + tags: | + # latest yes + type=raw,value=latest,enable=${{ !inputs.DEV_RELEASE }} + + # dispatch add x1.x.x-amd64 + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=300,prefix=,suffix=-amd64,value=${{ env.IMAGE_VERSION }} + + # dispatch add amd64-development + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == true }},priority=300,prefix=,suffix=-development,value=amd64 + + # tag add tag-arm64 + type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push'}},priority=600,prefix=,suffix=-amd64,event=tag + + # add development tag + type=raw,enable=${{ inputs.DEV_RELEASE }},priority=400,prefix=,suffix=,value=development + flavor: | + latest=${{ !inputs.DEV_RELEASE }} + + # # + # Release › Dockerhub › Checkpoint › Amd64 + # # + + - name: '⚠️ Checkpoint' + id: task_release_dh_checkpoint + run: | + echo "registry ............. Github" + echo "github.actor.......... ${{ github.actor }}" + echo "github.ref ........... ${{ github.ref }}" + echo "github.ref_name ...... ${{ github.ref_name }}" + echo "github.event_name .... ${{ github.event_name }}" + echo "inputs.DRY_RUN ....... ${{ inputs.DRY_RUN }}" + echo "env.AUTHOR ........... ${{ env.IMAGE_AUTHOR }}" + echo "tags ................. ${{ steps.task_release_dh_meta.outputs.tags }}" + echo "labels ............... ${{ steps.task_release_dh_meta.outputs.labels }}" + + # # + # Release › Dockerhub › Build and Push › Amd64 + # # + + - name: '📦 Build & Push (linux/amd64)' + id: task_release_dh_push + uses: docker/build-push-action@v6 + if: ( github.event_name == 'workflow_dispatch' && inputs.DRY_RUN == false ) || ( github.event_name == 'push' ) + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.task_release_dh_meta.outputs.tags }} + labels: ${{ steps.task_release_dh_meta.outputs.labels }} diff --git a/.github/workflows/deploy-docker-github.yml b/.github/workflows/deploy-docker-github.yml new file mode 100644 index 00000000..ee8ad7a2 --- /dev/null +++ b/.github/workflows/deploy-docker-github.yml @@ -0,0 +1,674 @@ +# # +# @type github workflow +# @author Aetherinox +# @url https://github.com/Aetherinox +# @usage deploys docker container to github and send message to discord +# upload this workflow to both the `main` branch of the tvapp repository +# @secrets secrets.ADMINSERV_GPG_KEY_ASC gpg private key (armored) | BEGIN PGP PRIVATE KEY BLOCK +# secrets.ADMINSERV_GPG_PASSPHRASE gpg private key passphrase +# secrets.IMAGE_GHCR_TOKEN github personal access token (classic) with package:write permission +# secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_RELEASES Discord webhook to report releases from github to discord +# # + +name: "📦 Deploy › Docker › Github" +run-name: "📦 Deploy › Docker › Github" + +# # +# Triggers +# # + +on: + + # # + # Trigger › Workflow Dispatch + # + # If any values are not provided, will use fallback env variable + # # + + workflow_dispatch: + inputs: + + # # + # Image Name + # + # used in github image path + # ghcr.io/${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + # # + + IMAGE_NAME: + description: '📦 Image Name' + required: true + default: 'tvapp2' + type: string + + # # + # Image Author + # + # used in github image path + # ghcr.io/${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + # # + + IMAGE_AUTHOR: + description: '🪪 Image Author' + required: true + default: 'Aetherinox' + type: string + + # # + # Image Version + # + # used to create new release tag, and add version to docker image name + # # + + IMAGE_VERSION: + description: '🏷️ Image Version' + required: true + default: '1.0.0' + type: string + + # # + # Image ghcr username + # + # this is the user to sign into ghcr as. + # # + + IMAGE_GHCR_USERNAME: + description: '🪪 ghcr.io Username' + required: true + default: 'Aetherinox' + type: string + + # # + # true no changes to the repo will be made + # false workflow will behave normally, and push any changes detected to the files + # # + + DRY_RUN: + description: '🐛 Dry Run (Debug)' + required: true + default: false + type: boolean + + # # + # true released version will be marked as a development build and will have the v1.x.x-development tag instead of -latest + # false release version will be marked with -latest docker tag + # # + + DEV_RELEASE: + description: '🧪 Development Release' + required: true + default: false + type: boolean + + # # + # Trigger › Push + # # + + push: + tags: + - '*' + +# # +# Environment Vars +# # + +env: + IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'tvapp2' }} + IMAGE_AUTHOR: ${{ github.event.inputs.IMAGE_AUTHOR || 'Aetherinox' }} + IMAGE_VERSION: ${{ github.event.inputs.IMAGE_VERSION || '1.0.0' }} + IMAGE_GHCR_USERNAME: ${{ github.event.inputs.IMAGE_GHCR_USERNAME || 'Aetherinox' }} + BOT_NAME_1: EuropaServ + BOT_NAME_DEPENDABOT: dependabot[bot] + +# # +# Jobs +# +# The way pushed docker containers on Github work, the most recent image built goes at the top. +# We will use the order below which builds the :latest image last so that it appears at the very +# top of the packages page. +# # + +jobs: + + # # + # Job › Create Tag + # # + + job-docker-release-tags-create: + name: >- + 📦 Release › Create Tag + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + attestations: write + id-token: write + steps: + + # # + # Release › Tags › Start + # # + + - name: '🏳️ Start' + id: task_release_tags_start + run: | + echo "Creating Tag" + + # # + # Release › Tags › Checkout + # # + + - name: '✅ Checkout' + id: task_release_tags_checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # # + # Release › Tags › Fix Permissions + # # + + - name: '#️⃣ Manage Permissions' + id: task_release_tags_permissions + run: | + find ./ -name 'run' -exec chmod 755 {} \; + WRONG_PERM=$(find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print) + if [ -n "${WRONG_PERM}" ]; then + echo "⚠️⚠️⚠️ Permissions are invalid ⚠️⚠️⚠️" + for i in ${WRONG_PERM}; do + echo "::error file=${i},line=1,title=Missing Executable Bit::This file needs to be set as executable!" + done + exit 1 + else + echo "✅✅✅ Executable permissions are OK ✅✅✅" + fi + + # # + # Release › Tags › Create Tag + # + # only called in dispatch mode + # # + + - uses: rickstaa/action-create-tag@v1 + id: task_release_tags_create + if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false ) + with: + tag: "${{ env.IMAGE_VERSION }}" + tag_exists_error: false + message: '${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}' + gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }} + gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }} + + # # + # Job › Docker Release › Github › Arm64 + # # + + job-docker-release-github-arm64: + name: >- + 📦 Release › Github › Arm64 + runs-on: ubuntu-latest + needs: [ job-docker-release-tags-create ] + permissions: + contents: write + packages: write + attestations: write + id-token: write + steps: + + # # + # Release › Github › Start › Arm64 + # # + + - name: '🏳️ Start' + id: task_release_gh_start + run: | + echo "Starting Github Docker arm64" + + # # + # Release › Get Timestamp + # # + + - name: '🕛 Get Timestamp' + id: task_release_set_timestamp + run: | + echo "NOW=$(date +'%m-%d-%Y %H:%M:%S')" >> $GITHUB_ENV + echo "NOW_SHORT=$(date +'%m-%d-%Y')" >> $GITHUB_ENV + echo "NOW_LONG=$(date +'%m-%d-%Y %H:%M')" >> $GITHUB_ENV + echo "NOW_DOCKER_LABEL=$(date +'%Y%m%d')" >> $GITHUB_ENV + + # # + # Release › Github › Checkout › Arm64 + # # + + - name: '✅ Checkout' + id: task_release_gh_checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # # + # Release › Github › Install Dependencies + # # + + - name: '📦 Install Dependencies' + id: task_release_gh_dependencies + run: + sudo apt-get install -qq dos2unix + + # # + # Release › Github › Execute dos2unix + # # + + - name: '🔐 Apply dos2unix' + id: task_release_gh_dos2unix + run: | + echo "⚠️⚠️⚠️ Running DOS2UNIX ⚠️⚠️⚠️" + find ./ \( -path "./.git" -o -path "./docs" -o -path "./.github" -o -path "*.png" -o -path "*.jpg" \) -prune -o -name '*' -print | xargs dos2unix -- + echo "✅✅✅ Completed DOS2UNIX ✅✅✅" + + # # + # Release › Github › Fix Permissions + # # + + - name: '#️⃣ Manage Permissions' + id: task_release_gh_permissions + run: | + find ./ -name 'run' -exec chmod 755 {} \; + WRONG_PERM=$(find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print) + if [ -n "${WRONG_PERM}" ]; then + echo "⚠️⚠️⚠️ Permissions are invalid ⚠️⚠️⚠️" + for i in ${WRONG_PERM}; do + echo "::error file=${i},line=1,title=Missing Executable Bit::This file needs to be set as executable!" + done + exit 1 + else + echo "✅✅✅ Executable permissions are OK ✅✅✅" + fi + + # # + # Release › Github › QEMU › Arm64 + # # + + - name: '⚙️ Set up QEMU' + id: task_release_gh_qemu + uses: docker/setup-qemu-action@v3 + + # # + # Release › Github › Setup BuildX › Arm64 + # # + + - name: '⚙️ Setup Buildx' + id: task_release_gh_buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + driver-opts: 'image=moby/buildkit:latest' + + # # + # Release › Github › Registry Login › Arm64 + # # + + - name: '⚙️ Login to Github' + id: task_release_gh_registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ env.IMAGE_GHCR_USERNAME }} + password: ${{ secrets.IMAGE_GHCR_TOKEN }} + + # # + # Release › Github › Meta › Arm64 + # # + + - name: '🔨 Github: Meta - Arm64' + id: task_release_gh_meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + tags: | + # latest no + type=raw,value=latest,enable=false + # dispatch add x1.x.x-arm64 + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=300,prefix=,suffix=-arm64,value=${{ env.IMAGE_VERSION }} + # dispatch add arm64-development + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == true }},priority=300,prefix=,suffix=-development,value=arm64 + # tag add tag-arm64 + type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=-arm64,event=tag + flavor: | + latest=false + labels: | + org.opencontainers.image.VERSION=${{ env.IMAGE_VERSION }} + org.opencontainers.image.BUILDDATE=${{ env.NOW_DOCKER_LABEL }} + org.opencontainers.image.licenses=MIT + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.vendor=${{ env.IMAGE_AUTHOR }} + org.opencontainers.image.ref.name=${{ env.GIT_REF }} + + # # + # Release › Github › Checkpoint › Arm64 + # # + + - name: '⚠️ Checkpoint' + id: task_release_gh_checkpoint + run: | + echo "registry ............. Github" + echo "github.actor.......... ${{ github.actor }}" + echo "github.ref ........... ${{ github.ref }}" + echo "github.ref_name ...... ${{ github.ref_name }}" + echo "github.event_name .... ${{ github.event_name }}" + echo "inputs.DRY_RUN ....... ${{ inputs.DRY_RUN }}" + echo "env.AUTHOR ........... ${{ env.IMAGE_AUTHOR }}" + echo "tags ................. ${{ steps.task_release_gh_meta.outputs.tags }}" + echo "labels ............... ${{ steps.task_release_gh_meta.outputs.labels }}" + + # # + # Release › Github › Build and Push › Arm64 + # # + + - name: '📦 Build & Push (linux/arm64)' + id: task_release_gh_push + uses: docker/build-push-action@v6 + if: ( github.event_name == 'workflow_dispatch' && inputs.DRY_RUN == false ) || ( github.event_name == 'push' ) + with: + context: . + file: Dockerfile.aarch64 + platforms: linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.task_release_gh_meta.outputs.tags }} + labels: ${{ steps.task_release_gh_meta.outputs.labels }} + + # # + # Release › Get Weekly Commits + # # + + - name: '🕛 Get Weekly Commit List' + id: task_release_set_weekly_commit_list + run: | + echo 'WEEKLY_COMMITS<<EOF' >> $GITHUB_ENV + git log --format="[\`%h\`](${{ github.server_url }}/${{ github.repository }}/commit/%H) %s - %an" --since=7.days >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + + # # + # Release › Notify Github + # # + + - name: '🔔 Send Discord Webhook Message' + uses: tsickert/discord-webhook@v6.0.0 + if: success() + with: + username: 'Io' + avatar-url: 'https://i.imgur.com/8BVDkla.jpg' + webhook-url: ${{ secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_RELEASES }} + embed-title: "📦 **Deploy › Docker › Github Workflow Ran**" + embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + embed-thumbnail-url: 'https://i.imgur.com/zDIzE8T.jpg' + embed-description: | + ## 📦 Docker › Deploy ${{ job.status == 'success' && '✅' || '❌' }} › `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}` + + A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github. The image is available at: + - https://github.com/${{ github.repository }}/pkgs/container/${{ env.IMAGE_NAME }} + + - Docker Image: `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}` + - Version: `${{ env.IMAGE_VERSION }}` + - Pull URL: https://ghcr.io/${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + - Branch: `${{ github.ref_name }}` + - Workflow: `${{ github.workflow }} (#${{github.run_number}})` + - Triggered By: `${{ github.actor }}` + + ### Tags + -# This docker image will use the following tags: + + ``` + ${{ steps.task_release_gh_meta.outputs.tags }} + ``` + + ### Labels + -# This docker image embeds the following labels: + + ``` + ${{ steps.task_release_gh_meta.outputs.labels }} + ``` + embed-color: ${{ job.status == 'success' && '5763719' || '15418782' }} + embed-footer-text: "Completed at ${{ env.NOW }} UTC" + embed-timestamp: "${{ env.NOW_LONG }}" + embed-author-name: "${{ github.event.release.author.name }}" + embed-author-url: "${{ github.event.release.author.html_url }}" + embed-author-icon-url: "${{ github.event.release.author.avatar_url }}" + + # # + # Job › Docker Release › Github › Amd64 + # # + + job-docker-release-github-amd64: + name: >- + 📦 Release › Github › Amd64 + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + attestations: write + id-token: write + needs: [ job-docker-release-tags-create, job-docker-release-github-arm64 ] + steps: + + # # + # Release › Github › Start › Amd64 + # # + + - name: '🏳️ Start' + id: task_release_gh_start + run: | + echo "Starting Github docker release" + + # # + # Release › Get Timestamp + # # + + - name: '🕛 Get Timestamp' + id: task_release_set_timestamp + run: | + echo "NOW=$(date +'%m-%d-%Y %H:%M:%S')" >> $GITHUB_ENV + echo "NOW_SHORT=$(date +'%m-%d-%Y')" >> $GITHUB_ENV + echo "NOW_LONG=$(date +'%m-%d-%Y %H:%M')" >> $GITHUB_ENV + echo "NOW_DOCKER_LABEL=$(date +'%Y%m%d')" >> $GITHUB_ENV + + # # + # Release › Github › Checkout › Amd64 + # # + + - name: '✅ Checkout' + id: task_release_gh_checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # # + # Release › Github › Install Dependencies + # # + + - name: '📦 Install Dependencies' + id: task_release_gh_dependencies + run: + sudo apt-get install -qq dos2unix + + # # + # Release › Github › Execute dos2unix + # # + + - name: '🔐 Apply dos2unix' + id: task_release_gh_dos2unix + run: | + find ./ \( -path "./.git" -o -path "./docs" -o -path "./.github" -o -path "*.png" -o -path "*.jpg" \) -prune -o -name '*' -print | xargs dos2unix -- + + # # + # Release › Github › Fix Permissions + # # + + - name: '#️⃣ Manage Permissions' + id: task_release_gh_permissions + run: | + find ./ -name 'run' -exec chmod 755 {} \; + WRONG_PERM=$(find ./ -path "./.git" -prune -o \( -name "run" -o -name "finish" -o -name "check" \) -not -perm -u=x,g=x,o=x -print) + if [ -n "${WRONG_PERM}" ]; then + echo "⚠️⚠️⚠️ Permissions are invalid ⚠️⚠️⚠️" + for i in ${WRONG_PERM}; do + echo "::error file=${i},line=1,title=Missing Executable Bit::This file needs to be set as executable!" + done + exit 1 + else + echo "✅✅✅ Executable permissions are OK ✅✅✅" + fi + + # # + # Release › Github › QEMU › Amd64 + # # + + - name: '⚙️ Set up QEMU' + id: task_release_gh_qemu + uses: docker/setup-qemu-action@v3 + + # # + # Release › Github › Setup BuildX › Amd64 + # # + + - name: '⚙️ Setup Buildx' + id: task_release_gh_buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + driver-opts: 'image=moby/buildkit:latest' + + # # + # Release › Github › Registry Login › Amd64 + # # + + - name: '⚙️ Login to Github' + id: task_release_gh_registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ env.IMAGE_GHCR_USERNAME }} + password: ${{ secrets.IMAGE_GHCR_TOKEN }} + + # # + # Release › Github › Meta › Amd64 + # # + + - name: '🔨 Github: Meta - Amd64' + id: task_release_gh_meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + tags: | + # latest yes + type=raw,value=latest,enable=${{ !inputs.DEV_RELEASE }} + # dispatch add x1.x.x-amd64 + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=300,prefix=,suffix=-amd64,value=${{ env.IMAGE_VERSION }} + # dispatch add amd64-development + type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == true }},priority=300,prefix=,suffix=-development,value=amd64 + # tag add tag-arm64 + type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push'}},priority=600,prefix=,suffix=-amd64,event=tag + # add development tag + type=raw,enable=${{ inputs.DEV_RELEASE }},priority=400,prefix=,suffix=,value=development + flavor: | + latest=${{ !inputs.DEV_RELEASE }} + labels: | + org.opencontainers.image.VERSION=${{ env.IMAGE_VERSION }} + org.opencontainers.image.BUILDDATE=${{ env.NOW_DOCKER_LABEL }} + org.opencontainers.image.licenses=MIT + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.vendor=${{ env.IMAGE_AUTHOR }} + org.opencontainers.image.ref.name=${{ env.GIT_REF }} + + # # + # Release › Github › Checkpoint › Amd64 + # # + + - name: '⚠️ Checkpoint' + id: task_release_gh_checkpoint + run: | + echo "registry ............. Github" + echo "github.actor.......... ${{ github.actor }}" + echo "github.ref ........... ${{ github.ref }}" + echo "github.ref_name ...... ${{ github.ref_name }}" + echo "github.event_name .... ${{ github.event_name }}" + echo "inputs.DRY_RUN ....... ${{ inputs.DRY_RUN }}" + echo "env.AUTHOR ........... ${{ env.IMAGE_AUTHOR }}" + echo "tags ................. ${{ steps.task_release_gh_meta.outputs.tags }}" + echo "labels ............... ${{ steps.task_release_gh_meta.outputs.labels }}" + + # # + # Release › Github › Build and Push › Amd64 + # # + + - name: '📦 Build & Push (linux/amd64)' + id: task_release_gh_push + uses: docker/build-push-action@v6 + if: ( github.event_name == 'workflow_dispatch' && inputs.DRY_RUN == false ) || ( github.event_name == 'push' ) + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.task_release_gh_meta.outputs.tags }} + labels: ${{ steps.task_release_gh_meta.outputs.labels }} + + # # + # Release › Get Weekly Commits + # # + + - name: '🕛 Get Weekly Commit List' + id: task_release_set_weekly_commit_list + run: | + echo 'WEEKLY_COMMITS<<EOF' >> $GITHUB_ENV + git log --format="[\`%h\`](${{ github.server_url }}/${{ github.repository }}/commit/%H) %s - %an" --since=7.days >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + + # # + # Release › Notify Github + # # + + - name: '🔔 Send Discord Webhook Message' + uses: tsickert/discord-webhook@v6.0.0 + if: success() + with: + username: 'Io' + avatar-url: 'https://i.imgur.com/8BVDkla.jpg' + webhook-url: ${{ secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_RELEASES }} + embed-title: "📦 **Deploy › Docker › Github Workflow Ran**" + embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + embed-thumbnail-url: 'https://i.imgur.com/zDIzE8T.jpg' + embed-description: | + ## 📦 Docker › Deploy ${{ job.status == 'success' && '✅' || '❌' }} › `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}` + + A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github. The image is available at: + - https://github.com/${{ github.repository }}/pkgs/container/${{ env.IMAGE_NAME }} + + - Docker Image: `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}` + - Version: `${{ env.IMAGE_VERSION }}` + - Pull URL: https://ghcr.io/${{ env.IMAGE_AUTHOR }}/${{ env.IMAGE_NAME }} + - Branch: `${{ github.ref_name }}` + - Workflow: `${{ github.workflow }} (#${{github.run_number}})` + - Triggered By: `${{ github.actor }}` + + ### Tags + -# This docker image will use the following tags: + + ``` + ${{ steps.task_release_gh_meta.outputs.tags }} + ``` + + ### Labels + -# This docker image embeds the following labels: + + ``` + ${{ steps.task_release_gh_meta.outputs.labels }} + ``` + embed-color: ${{ job.status == 'success' && '5763719' || '15418782' }} + embed-footer-text: "Completed at ${{ env.NOW }} UTC" + embed-timestamp: "${{ env.NOW_LONG }}" + embed-author-name: "${{ github.event.release.author.name }}" + embed-author-url: "${{ github.event.release.author.html_url }}" + embed-author-icon-url: "${{ github.event.release.author.avatar_url }}" diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml deleted file mode 100644 index 30018775..00000000 --- a/.github/workflows/deploy-docker.yml +++ /dev/null @@ -1,516 +0,0 @@ -# # -# @type github workflow -# @desc deploys docker container -# @author Aetherinox -# @url https://github.com/Aetherinox -# # - -name: "⚙️ Deploy › Docker › Main" -run-name: "⚙️ Deploy › Docker › Main" - -# # -# triggers -# # - -on: - - # # - # Trigger > Workflow Dispatch - # # - - workflow_dispatch: - inputs: - - IMAGE_NAME: - description: "📦 Image Name" - required: true - default: 'thetvapp-docker' - type: string - - IMAGE_AUTHOR: - description: "📦 Image Author" - required: true - default: 'aetherinox' - type: string - - # # - # true: runs all actions, even ones not scheduled - # false: only scheduled tasks will run - # # - - PRINT_ONLY: - description: "📑 Print Debugs Only" - required: true - default: false - type: boolean - - # # - # Trigger > Push - # # - - push: - tags: - - '*' - -# # -# environment variables -# # - -env: - IMAGE_NAME: alpine-base - IMAGE_AUTHOR: Aetherinox - BOT_NAME_1: EuropaServ - BOT_NAME_DEPENDABOT: dependabot[bot] - -# # -# jobs -# -# The way pushed docker containers on Github work, the most recent image built goes at the top. -# We will use the order below which builds the :latest image last so that it appears at the very -# top of the packages page. -# # - -jobs: - - # # - # Job > Docker Release > Github - # # - - job-docker-release-github-php: - name: >- - 📦 Release › Github › PHP - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - # # - # Release > Github > Start - # # - - - name: "✅ Start" - id: task_release_gh_start - run: | - echo "Starting Github docker release for image PHP" - - # # - # Release > Github > Checkout - # # - - - name: "☑️ Checkout" - id: task_release_gh_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # Release > Github > QEMU - # # - - - name: "⚙️ Set up QEMU" - id: task_release_gh_qemu - uses: docker/setup-qemu-action@v3 - - # # - # Release > Github > Setup BuildX - # # - - - name: "⚙️ Setup Buildx" - id: task_release_gh_buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - driver-opts: 'image=moby/buildkit:v0.10.5' - - # # - # Release > Github > Registry Login - # # - - - name: "⚙️ Login to Github" - id: task_release_gh_registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.SELF_TOKEN_CL }} - - # # - # Release > Github > Meta - # # - - - name: "🔨 Docker meta" - id: task_release_gh_meta - uses: docker/metadata-action@v5 - with: - images: | - ghcr.io/${{ inputs.IMAGE_AUTHOR || env.IMAGE_AUTHOR }}/docker-${{ inputs.IMAGE_NAME || env.IMAGE_NAME }} - tags: | - type=ref,enable=true,priority=600,prefix=,suffix=-php,event=tag - flavor: | - latest=false - - # # - # Release > Github > Debug - # # - - - name: "🪪 Debug › Print" - id: task_release_gh_print - run: | - echo "registry ............. Github" - echo "github.actor.......... ${{ github.actor }}" - echo "github.ref ........... ${{ github.ref }}" - echo "github.event_name .... ${{ github.event_name }}" - echo "tags ................. ${{ steps.task_release_gh_meta.outputs.tags }}" - echo "labels ............... ${{ steps.task_release_gh_meta.outputs.labels }}" - - # # - # Release > Github > Build and Push - # # - - - name: "📦 Build and push" - id: task_release_gh_push - uses: docker/build-push-action@v6 - if: ( github.event_name == 'workflow_dispatch' && inputs.PRINT_ONLY == 'false' ) || ( github.event_name == 'push' ) - with: - context: . - file: Dockerfile-php.template - platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.task_release_gh_meta.outputs.tags }} - labels: ${{ steps.task_release_gh_meta.outputs.labels }} - - # # - # Job > Docker Release > Github - # # - - job-docker-release-dockerhub-php: - name: >- - 📦 Release › Dockerhub › PHP - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - # # - # Release > Dockerhub > Start - # # - - - name: "✅ Start" - id: task_release_dh_start - run: | - echo "Starting Dockerhub Release" - - # # - # Release > Dockerhub > Checkout - # # - - - name: "☑️ Checkout" - id: task_release_dh_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # Release > Dockerhub > QEMU - # # - - - name: "⚙️ Set up QEMU" - id: task_release_dh_qemu - uses: docker/setup-qemu-action@v3 - - # # - # Release > Dockerhub > Setup BuildX - # # - - - name: "⚙️ Setup Buildx" - id: task_release_dh_buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - driver-opts: 'image=moby/buildkit:v0.10.5' - - # # - # Release > Dockerhub > Registry Login - # # - - - name: "⚙️ Login to DockerHub" - id: task_release_dh_registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ inputs.IMAGE_AUTHOR || env.IMAGE_AUTHOR }} - password: ${{ secrets.SELF_DOCKERHUB_TOKEN }} - - # # - # Release > Dockerhub > Meta - # # - - - name: "🔨 Docker meta" - id: task_release_dh_meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ inputs.IMAGE_AUTHOR || env.IMAGE_AUTHOR }}/${{ inputs.IMAGE_NAME || env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable=false - type=ref,enable=true,priority=600,prefix=,suffix=-php,event=tag - flavor: | - latest=false - - # # - # Release > Dockerhub > Debug - # # - - - name: "🪪 Debug › Print" - id: task_release_dh_print - run: | - echo "registry ............. Dockerhub" - echo "github.actor.......... ${{ github.actor }}" - echo "github.ref ........... ${{ github.ref }}" - echo "github.event_name .... ${{ github.event_name }}" - echo "tags ................. ${{ steps.task_release_dh_meta.outputs.tags }}" - echo "labels ............... ${{ steps.task_release_dh_meta.outputs.labels }}" - - # # - # Release > Dockerhub > Build and Push - # # - - - name: "📦 Build and push" - id: task_release_dh_push - uses: docker/build-push-action@v6 - if: ( github.event_name == 'workflow_dispatch' && inputs.PRINT_ONLY == 'false' ) || ( github.event_name == 'push' ) - with: - context: . - file: Dockerfile-php.template - platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.task_release_dh_meta.outputs.tags }} - labels: ${{ steps.task_release_dh_meta.outputs.labels }} - - # # - # Job > Docker Release > Github - # # - - job-docker-release-github-main: - name: >- - 📦 Release › Github › Main - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - needs: [ job-docker-release-github-php, job-docker-release-dockerhub-php ] - steps: - - # # - # Release > Github > Start - # # - - - name: "✅ Start" - id: task_release_gh_start - run: | - echo "Starting Github docker release" - - # # - # Release > Github > Checkout - # # - - - name: "☑️ Checkout" - id: task_release_gh_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # Release > Github > QEMU - # # - - - name: "⚙️ Set up QEMU" - id: task_release_gh_qemu - uses: docker/setup-qemu-action@v3 - - # # - # Release > Github > Setup BuildX - # # - - - name: "⚙️ Setup Buildx" - id: task_release_gh_buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - driver-opts: 'image=moby/buildkit:v0.10.5' - - # # - # Release > Github > Registry Login - # # - - - name: "⚙️ Login to Github" - id: task_release_gh_registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.SELF_TOKEN_CL }} - - # # - # Release > Github > Meta - # # - - - name: "🔨 Docker meta" - id: task_release_gh_meta - uses: docker/metadata-action@v5 - with: - images: | - ghcr.io/${{ inputs.IMAGE_AUTHOR || env.IMAGE_AUTHOR }}/docker-${{ inputs.IMAGE_NAME || env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable=${{ endsWith(github.ref, 'main') }} - type=ref,event=tag - - # # - # Release > Github > Debug - # # - - - name: "🪪 Debug › Print" - id: task_release_gh_print - run: | - echo "registry ............. Github" - echo "github.actor.......... ${{ github.actor }}" - echo "github.ref ........... ${{ github.ref }}" - echo "github.event_name .... ${{ github.event_name }}" - echo "tags ................. ${{ steps.task_release_gh_meta.outputs.tags }}" - echo "labels ............... ${{ steps.task_release_gh_meta.outputs.labels }}" - - # # - # Release > Github > Build and Push - # # - - - name: "📦 Build and push" - id: task_release_gh_push - uses: docker/build-push-action@v6 - if: ( github.event_name == 'workflow_dispatch' && inputs.PRINT_ONLY == 'false' ) || ( github.event_name == 'push' ) - with: - context: . - file: Dockerfile - platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.task_release_gh_meta.outputs.tags }} - labels: ${{ steps.task_release_gh_meta.outputs.labels }} - - # # - # Job > Docker Release > Github - # # - - job-docker-release-dockerhub-main: - name: >- - 📦 Release › Dockerhub › Main - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - needs: [ job-docker-release-github-php, job-docker-release-dockerhub-php ] - steps: - - # # - # Release > Dockerhub > Start - # # - - - name: "✅ Start" - id: task_release_dh_start - run: | - echo "Starting Github docker release" - - # # - # Release > Dockerhub > Checkout - # # - - - name: "☑️ Checkout" - id: task_release_dh_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # Release > Dockerhub > QEMU - # # - - - name: "⚙️ Set up QEMU" - id: task_release_dh_qemu - uses: docker/setup-qemu-action@v3 - - # # - # Release > Dockerhub > Setup BuildX - # # - - - name: "⚙️ Setup Buildx" - id: task_release_dh_buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - driver-opts: 'image=moby/buildkit:v0.10.5' - - # # - # Release > Dockerhub > Registry Login - # # - - - name: "⚙️ Login to DockerHub" - id: task_release_dh_registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ inputs.IMAGE_AUTHOR || env.IMAGE_AUTHOR }} - password: ${{ secrets.SELF_DOCKERHUB_TOKEN }} - - # # - # Release > Dockerhub > Meta - # # - - - name: "🔨 Docker meta" - id: task_release_dh_meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ inputs.IMAGE_AUTHOR || env.IMAGE_AUTHOR }}/${{ inputs.IMAGE_NAME || env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable=${{ endsWith(github.ref, 'main') }} - type=ref,event=tag - - # # - # Release > Dockerhub > Debug - # # - - - name: "🪪 Debug › Print" - id: task_release_dh_print - run: | - echo "registry ............. Dockerhub" - echo "github.actor.......... ${{ github.actor }}" - echo "github.ref ........... ${{ github.ref }}" - echo "github.event_name .... ${{ github.event_name }}" - echo "tags ................. ${{ steps.task_release_dh_meta.outputs.tags }}" - echo "labels ............... ${{ steps.task_release_dh_meta.outputs.labels }}" - - # # - # Release > Dockerhub > Build and Push - # # - - - name: "📦 Build & Push" - id: task_release_dh_push - uses: docker/build-push-action@v6 - if: ( github.event_name == 'workflow_dispatch' && inputs.PRINT_ONLY == 'false' ) || ( github.event_name == 'push' ) - with: - context: . - file: Dockerfile - platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.task_release_dh_meta.outputs.tags }} - labels: ${{ steps.task_release_dh_meta.outputs.labels }} diff --git a/.github/workflows/issues-accept.yml b/.github/workflows/issues-accept.yml deleted file mode 100644 index 73843069..00000000 --- a/.github/workflows/issues-accept.yml +++ /dev/null @@ -1,72 +0,0 @@ -# # -# @type github workflow -# @desc adds a label to a PR when the command "/accept" is typed in the issue comments -# do not attempt to use env variables in if condition. -# do not accept to change GITHUB_TOKEN. -# @author Aetherinox -# @url https://github.com/Aetherinox -# # - -name: "🎫 Issue › Accept" -run-name: "🎫 Issue › Accept" - -# # -# triggers -# # - -on: - issue_comment: - types: [created] - -# # -# environment variables -# # - -env: - LABEL_ACCEPT: "Status 𐄂 Accepted" - - BOT_NAME_1: AdminServ - BOT_NAME_2: AdminServX - BOT_NAME_3: EuropaServ - BOT_NAME_DEPENDABOT: dependabot[bot] - -# # -# jobs -# # - -jobs: - - # # - # Job [ Deploy ] - # # - - deploy: - if: contains(github.event.comment.body, '/accept') && github.event.comment.user.login == 'Aetherinox' - runs-on: ubuntu-latest - steps: - - # # - # Add Label to accepted PR - # # - - - name: >- - 🏷️ Assign Label › ${{ env.LABEL_ACCEPT }} - run: gh issue edit "$NUMBER" --add-label "$LABELS" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.issue.number }} - LABELS: ${{ env.LABEL_ACCEPT }} - - # # - # Add assignee to accepted PR - # # - - - name: >- - 🏷️ Assign Assignee › ${{ github.repository_owner }} - run: gh issue edit "$NUMBER" --add-assignee "$ASSIGNEE" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.issue.number }} - ASSIGNEE: ${{ github.repository_owner }} diff --git a/.github/workflows/issues-new.yml b/.github/workflows/issues-new.yml deleted file mode 100644 index 7f237ad9..00000000 --- a/.github/workflows/issues-new.yml +++ /dev/null @@ -1,891 +0,0 @@ -# # -# @type github workflow -# @desc searches a new issues title and body for certain keywords and assigns a label -# sets the assignee for the issue to the repository owner -# @author Aetherinox -# @url https://github.com/Aetherinox -# -# requires the following labels to be created in your repo: -# - bug -# - feature -# - urgent -# - roadmap -# # - -name: "🎫 Issue › New" -run-name: "🎫 Issue › New › ${{ github.event.issue.number }}: ${{ github.event.issue.title }}" - -# # -# triggers -# # - -on: - issues: - types: - - reopened - - opened - -# # -# environment variables -# # - -env: - PREFIX_BUG: "Bug" - PREFIX_DEPENDENCY: "Dependency" - PREFIX_DOCS: "Docs" - PREFIX_FEATURE: "Feature" - PREFIX_GIT: "Git Action" - PREFIX_PR: "PR" - PREFIX_ROADMAP: "Roadmap" - PREFIX_INTERNAL: "Internal" - PREFIX_URGENT: "Urgent" - - LABEL_BUG: "Type ◦ Bug" - LABEL_DEPENDENCY: "Type ◦ Dependency" - LABEL_DOCS: "Type ◦ Docs" - LABEL_FEATURE: "Type ◦ Feature" - LABEL_GIT: "Type ◦ Git Action" - LABEL_PR: "Type ◦ Pull Request" - LABEL_ROADMAP: "Type ◦ Roadmap" - LABEL_INTERNAL: "Type ◦ Git Action" - LABEL_URGENT: "⚠ Urgent" - - BOT_NAME_1: AdminServ - BOT_NAME_2: AdminServX - BOT_NAME_3: EuropaServ - BOT_NAME_DEPENDABOT: dependabot[bot] - - LABELS_JSON: | - [ - { "name": "AC › Changes Made", "color": "8F1784", "description": "Requested changes have been made and are pending a re-scan" }, - { "name": "AC › Changes Required", "color": "8F1784", "description": "Requires changes to be made to the package before being accepted" }, - { "name": "AC › Failed", "color": "a61f2d", "description": "Autocheck failed to run through a complete cycle, requires investigation" }, - { "name": "AC › Needs Rebase", "color": "8F1784", "description": "Due to the permissions on the requesting repo, this pull request must be rebased by the author" }, - { "name": "AC › Passed", "color": "146b4a", "description": "Ready to be reviewed" }, - { "name": "AC › Review Required", "color": "8F1784", "description": "PR needs to be reviewed by another person, after the requested changes have been made" }, - { "name": "AC › Security Warning", "color": "761620", "description": "Does not conform to developer policies, or includes potentially dangerous code" }, - { "name": "AC › Skipped Scan", "color": "8F1784", "description": "Author has skipped code scan" }, - { "name": "Status 𐄂 Duplicate", "color": "75536b", "description": "Issue or pull request already exists" }, - { "name": "Status 𐄂 Accepted", "color": "2e7539", "description": "This pull request has been accepted" }, - { "name": "Status 𐄂 Autoclosed", "color": "3E0915", "description": "Originally stale and was autoclosed for no activity" }, - { "name": "Status 𐄂 Denied", "color": "ba4058", "description": "Pull request has been denied" }, - { "name": "Status 𐄂 Locked", "color": "550F45", "description": "Automatically locked by AdminServ for a prolonged period of inactivity" }, - { "name": "Status 𐄂 Need Info", "color": "2E3C4C", "description": "Not enough information to resolve" }, - { "name": "Status 𐄂 No Action", "color": "030406", "description": "Closed without any action being taken" }, - { "name": "Status 𐄂 Pending", "color": "984b12", "description": "Pending pull request" }, - { "name": "Status 𐄂 Released", "color": "1b6626", "description": "Issues or PR has been implemented and is now live" }, - { "name": "Status 𐄂 Reopened", "color": "8a6f14", "description": "A previously closed PR which has been re-opened" }, - { "name": "Status 𐄂 Review", "color": "9e1451", "description": "Currently pending review" }, - { "name": "Status 𐄂 Stale", "color": "928282", "description": "Has not had any activity in over 30 days" }, - { "name": "Type ◦ Bug", "color": "9a2c2c", "description": "Something isn't working" }, - { "name": "Type ◦ Dependency", "color": "243759", "description": "Item is associated to dependency" }, - { "name": "Type ◦ Docs", "color": "0e588d", "description": "Improvements or modifications to docs" }, - { "name": "Type ◦ Feature", "color": "3c4e93", "description": "Feature request" }, - { "name": "Type ◦ Git Action", "color": "030406", "description": "GitHub Action / workflow" }, - { "name": "Type ◦ Pull Request", "color": "8F1784", "description": "Normal pull request" }, - { "name": "Type ◦ Roadmap", "color": "8F1784", "description": "Feature or bug currently planned for implementation" }, - { "name": "Type ◦ Internal", "color": "A51994", "description": "Assigned items are for internal developer use" }, - { "name": "Build ◦ Desktop", "color": "c7ca4a", "description": "Specific to desktop" }, - { "name": "Build ◦ Linux", "color": "c7ca4a", "description": "Specific to Linux" }, - { "name": "Build ◦ MacOS", "color": "c7ca4a", "description": "Specific to MacOS" }, - { "name": "Build ◦ Mobile", "color": "c7ca4a", "description": "Specific to mobile" }, - { "name": "Build ◦ Web", "color": "c7ca4a", "description": "Specific to web" }, - { "name": "Build ◦ Windows", "color": "c7ca4a", "description": "Specific to Windows" }, - { "name": "› API", "color": "F99B50", "description": "Plugin API, CLI, browser JS API" }, - { "name": "› Auto-type", "color": "9141E0", "description": "Auto-type functionality in desktop apps" }, - { "name": "› Browser", "color": "9141E0", "description": "Browser plugins and passing data to <=> from app" }, - { "name": "› Customization", "color": "E3F0FC", "description": "Customizations: plugins, themes, configs" }, - { "name": "› Design", "color": "FA70DE", "description": "Design related queries" }, - { "name": "› Dist", "color": "FA70DE", "description": "Installers and other forms of software distribution" }, - { "name": "› Enterprise", "color": "11447a", "description": "Issues about collaboration, administration, and so on" }, - { "name": "› Hardware", "color": "5a7503", "description": "YubiKey, other tokens, biometrics" }, - { "name": "› Import/Export", "color": "F5FFCC", "description": "Import from and export to different file formats" }, - { "name": "› Improvement", "color": "185c98", "description": "Enhance an existing feature" }, - { "name": "› Performance", "color": "006b75", "description": "Web and desktop performance issues" }, - { "name": "› Plugin Request", "color": "FCE9CA", "description": "Requested changes should be implemented as a plugin" }, - { "name": "› Security", "color": "F75D39", "description": "Security issues" }, - { "name": "› Self-Hosting", "color": "fad8c7", "description": "Self-hosting installations and configs" }, - { "name": "› Storage", "color": "5319e7", "description": "Storage providers: Dropbox, Google, WebDAV, etc." }, - { "name": "› Updater", "color": "1BADDE", "description": "Auto-updater issues" }, - { "name": "› UX", "color": "1BADDE", "description": "UX and usability" }, - { "name": "› Website", "color": "fef2c0", "description": "Website related issues" }, - { "name": "⚠ Urgent", "color": "a8740e", "description": "Requires urgent attention" }, - { "name": "⚠ Announcement", "color": "DB4712", "description": "Announcements" }, - { "name": "📰 Progress Report", "color": "392297", "description": "Development updates" }, - { "name": "📦 Release", "color": "277542", "description": "Release announcements" }, - { "name": "✔️ Poll", "color": "972255", "description": "Community polls" }, - { "name": "❔ Question", "color": "FFFFFF", "description": "All questions" } - ] - -# # -# jobs -# # - -jobs: - - # # - # Job [ Verify / Create Labels ] - # - # This job will ensure you have labels already created in your repo. - # All labels come from the JSON table LABELS_JSON. - # # - - job-labels-create: - name: >- - 🎫 Labels › Verify Existing - runs-on: ubuntu-latest - steps: - - # # - # [ Create Labels ] Start - # # - - - name: >- - ✅ Start - id: task_label_create_start - run: | - echo "Assigning labels and assignees" - - # # - # [ Create Labels ] Checkout - # # - - - name: >- - ☑️ Checkout - id: task_label_create_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # [ Create Labels ] Verify Existing Labels - # # - - - name: >- - 🏷️ Verify Existing Labels - id: task_label_create_verify - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const labels = JSON.parse( process.env.LABELS_JSON ); - for ( const label of labels ) - { - try - { - await github.rest.issues.createLabel( - { - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - description: label.description || '', - color: label.color - }); - } - catch ( err ) - { - if ( err.status === 422 ) - { - console.log( `Label '${label.name}' already exists. Skipping.` ); - } - else - { - console.error( `Error creating label '${label.name}': ${err}` ); - } - } - } - - # # - # Job [ Assign Labels ] - # # - - job-assign-labels: - name: >- - 🏷️ Labels › Assign - needs: - - job-labels-create - runs-on: ubuntu-latest - permissions: - contents: 'read' - id-token: 'write' - issues: 'write' - steps: - - # # - # Assign > Get Issue Title - # # - - - name: >- - 🏷️ Get Issue Title - uses: actions/github-script@v7 - id: task_get_title - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - let iss_title = `${ context.payload.issue.title }`; - - core.setOutput( 'issue_title', iss_title ) - core.info( `Setting env issue title: ${ iss_title }` ) - - console.log( "\n\n" ) - - # # - # Labels > Bugs - # - # Title of issue is carried over from the previous step. - # # - - - name: >- - 🏷️ ${{ env.PREFIX_BUG }} › Assignment - uses: actions/github-script@v7 - id: task_issues_bugs - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - - const issueLabels = await github.rest.issues.listLabelsOnIssue( - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - let add_labels = issueLabels.data.map( label => label.name ); - - let iss_title = `${{ steps.task_get_title.outputs.issue_title }}` || `${ context.payload.issue.title }`; - let iss_body = `${ context.payload.issue.body }`; - let iss_author = `${ context.payload.issue.user.login }`; - - const iss_title_lc = iss_title.toLowerCase( ); - - console.log( "Bug Title ..................... " + iss_title ) - console.log( "Bug Output .................... " + `${{ steps.task_get_title.outputs.issue_title }}` ) - console.log( "Bug Payload ................... " + `${ context.payload.issue.title }` ) - - /* - Tags - */ - - const bug_tag = `${{ env.PREFIX_BUG }}:`; - const bug_lbl = `${{ env.LABEL_BUG }}`; - const feat_tag = `${{ env.PREFIX_FEATURE }}:`; - const feat_lbl = `${{ env.LABEL_FEATURE }}`; - const urgn_tag = `${{ env.PREFIX_URGENT }}:`; - const urgn_lbl = `${{ env.LABEL_URGENT }}`; - const road_tag = `${{ env.PREFIX_ROADMAP }}:`; - const road_lbl = `${{ env.LABEL_ROADMAP }}`; - - /* - Bugs - */ - - const words = [ "bug", "broke", "issue", "fail" ]; - const bTriggerWordInTitle = words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - - Regex: - https://regex101.com/r/Z99Gnq/2 - */ - - const findWordList = /^\b(?:I?\s*have\s*(?:a|an)\s*(?:issue|problem|bug))|(?:will\s*not\s*work)|(?:it\s*is\s*(?:broken|broke|stuck))|(?:found\s*(?:an?|the)\s*(?:bug|issue))|(?:can\s*I\s*fix\s*the\s*(?:bug|issue))|(?:(?:does not|doesn'?t|don'?t|won'?t|can'?t|can\s?not|will\s*not)\s*(?:work|load|function))|(?:it\s*(?:will\s?not|won'?t|can\s?not|can'?t))\s*(?:get|find)\s*the\s*(?:website|site|webpage|page)|(?:the\s*(?:window|frame)\s*is\s*(?:blank|white|empty|missing))\b$/igm; - const bFoundMatchTitle = Boolean( findWordList.test( iss_title ) ); - const bFoundMatchBody = Boolean( findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const bug_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const bug_bFoundPRTitle = Boolean( bug_findPRTitle.test( iss_title ) ); - - console.log( "Title Lowercase ............... " + iss_title_lc ) - console.log( "Startswith " + bug_tag.toLowerCase( ) + "................ " + iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) ) - console.log( "Title Includes Keyword ........ " + bTriggerWordInTitle ) - console.log( "Title Includes Regex .......... " + bFoundMatchTitle ) - console.log( "Body Includes Regex ........... " + bFoundMatchBody ) - console.log( "\n" ) - - /* - - Check if issue title matches the issue label "Bug:" - - Check if title contains word in words - */ - - if ( iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) || bTriggerWordInTitle || bFoundMatchTitle || bFoundMatchBody ) - { - - console.log( "⚠️ " + bug_tag + " ---------------------------------------" ) - console.log( "Already starts with " + bug_tag + " ......... " + iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + feat_tag + " ..... " + iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + urgn_tag + " ...... " + iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + road_tag + " ..... " + iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - - add_labels.push( `${ bug_lbl }` ); - - console.log( `Adding Tag ....................... ${ bug_lbl }` ) - console.log( "\n" ) - - if ( iss_author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ iss_author }` ) - - // Rename title to contain Bug: - // Make sure issue / pr title doesnt already contain a beginning title tag - - if ( iss_author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !bug_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - console.log( "Renaming Title" ) - console.log( `Old Title: .................. ${ iss_title }` ) - - const title = context.payload.issue.title - let title_new = title.replace( /^\s?bug\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?fail\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?issue\s*(.*?)\b/gi, '' ); - iss_title = `${ bug_tag } ${ title_new }`; - } - - console.log( `New Title: ...................... ${ iss_title }` ) - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - core.setOutput( 'issue_title', iss_title ) - console.log( "\n\n" ) - - # # - # Labels > Features - # - # Title of issue is carried over from the previous step. - # # - - - name: >- - 🏷️ ${{ env.PREFIX_FEATURE }} › Assignment - uses: actions/github-script@v7 - id: task_issues_features - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - - const issueLabels = await github.rest.issues.listLabelsOnIssue( - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - let add_labels = issueLabels.data.map( label => label.name ); - - let iss_title = `${{ steps.task_issues_bugs.outputs.issue_title }}` || `${ context.payload.issue.title }`; - let iss_body = `${ context.payload.issue.body }`; - - const iss_title_lc = iss_title.toLowerCase( ); - - console.log( "Feat Title .................... " + iss_title ) - console.log( "Feat Output ................... " + `${{ steps.task_issues_bugs.outputs.issue_title }}` ) - console.log( "Feat Payload .................. " + `${ context.payload.issue.title }` ) - - /* - Tags - */ - - const bug_tag = `${{ env.PREFIX_BUG }}:`; - const bug_lbl = `${{ env.LABEL_BUG }}`; - const feat_tag = `${{ env.PREFIX_FEATURE }}:`; - const feat_lbl = `${{ env.LABEL_FEATURE }}`; - const urgn_tag = `${{ env.PREFIX_URGENT }}:`; - const urgn_lbl = `${{ env.LABEL_URGENT }}`; - const road_tag = `${{ env.PREFIX_ROADMAP }}:`; - const road_lbl = `${{ env.LABEL_ROADMAP }}`; - - /* - Features - */ - - const words = [ "feature", "request", "add support" ]; - const bTriggerWordInTitle = words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - - Regex: - https://regex101.com/r/fR1Hm6/1 - */ - - const findWordList = /^(?:(?:request|include|see)\s*(?:an?|the?)\s*(?:feature|addon|addition|plugin))|(?:(?:add|see|get)\s*support\s*(?:for|with|of))|(?:can\s*we\s*get\s*(?:the|a)\s*(?:ability|feature))|(?:💡 Feature:)$/igm; - const bFoundMatchTitle = Boolean( findWordList.test( iss_title ) ); - const bFoundMatchBody = Boolean( findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const feat_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const feat_bFoundPRTitle = Boolean( feat_findPRTitle.test( iss_title ) ); - - console.log( "Title Lowercase ............... " + iss_title_lc ) - console.log( "Startswith " + feat_tag.toLowerCase( ) + "............ " + iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) ) - console.log( "Title Includes Keyword ........ " + bTriggerWordInTitle ) - console.log( "Title Includes Regex .......... " + bFoundMatchTitle ) - console.log( "Body Includes Regex ........... " + bFoundMatchBody ) - console.log( "\n" ) - - /* - - Check if issue title matches the issue label "Feature:" - - Check if title contains word in words - */ - - // change TAG per category - if ( iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) || bTriggerWordInTitle || bFoundMatchTitle || bFoundMatchBody ) - { - - console.log( "⚠️ " + feat_tag + " ---------------------------------------" ) - console.log( "Already starts with " + bug_tag + " ......... " + iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + feat_tag + " ..... " + iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + urgn_tag + " ...... " + iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + road_tag + " ..... " + iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - - // change LBL per category - add_labels.push( `${ feat_lbl }` ); - - console.log( `Adding Tag ....................... ${ feat_lbl }` ) - console.log( "\n" ) - - if ( iss_author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ iss_author }` ) - - // Rename title to contain Feature: - // Make sure issue / pr title doesnt already contain a beginning title tag - - if ( iss_author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !feat_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - console.log( "Renaming Title" ) - console.log( `Old Title: .................. ${ iss_title }` ) - - const title = context.payload.issue.title - let title_new = title.replace( /^\s?feature\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?request\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?add(.*?)\s?feature\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?add(.*?)\s?support\s*(.*?)\b/gi, '' ); - iss_title = `${ feat_tag } ${ title_new }`; // change TAG per category - } - - console.log( `New Title: ...................... ${ iss_title }` ) - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - core.setOutput( 'issue_title', iss_title ) - console.log( "\n\n" ) - - # # - # Labels > Urgent - # - # Title of issue is carried over from the previous step. - # # - - - name: >- - 🏷️ ${{ env.PREFIX_URGENT }} › Assignment - uses: actions/github-script@v7 - id: task_issues_urgent - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const issueLabels = await github.rest.issues.listLabelsOnIssue( - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - let add_labels = issueLabels.data.map( label => label.name ); - - let iss_title = `${{ steps.task_issues_features.outputs.issue_title }}` || `${ context.payload.issue.title }`; - let iss_body = `${ context.payload.issue.body }`; - - const iss_title_lc = iss_title.toLowerCase( ); - - console.log( "Urgn Title .................... " + iss_title ) - console.log( "Urgn Output ................... " + `${{ steps.task_issues_features.outputs.issue_title }}` ) - console.log( "Urgn Payload .................. " + `${ context.payload.issue.title }` ) - - /* - Tags - */ - - const bug_tag = `${{ env.PREFIX_BUG }}:`; - const bug_lbl = `${{ env.LABEL_BUG }}`; - const feat_tag = `${{ env.PREFIX_FEATURE }}:`; - const feat_lbl = `${{ env.LABEL_FEATURE }}`; - const urgn_tag = `${{ env.PREFIX_URGENT }}:`; - const urgn_lbl = `${{ env.LABEL_URGENT }}`; - const road_tag = `${{ env.PREFIX_ROADMAP }}:`; - const road_lbl = `${{ env.LABEL_ROADMAP }}`; - - /* - Urgent - */ - - const words = [ "urgent", "urgency", "emergency", "important", "critical" ]; - const bTriggerWordInTitle = words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - - Regex: - https://regex101.com/r/eE9tJX/2 - */ - - const findWordList = /(?:(?:this)?is\s*a?n?\s*?(?:emergency|urgent|important|vital|acute|crucial|grave|pressing|serious|top.?priority|high.?priority))|(?:reply|respond|answer|write|address)\s*(?:immediate|quick|asap|urgent|now|fast|(?:as)?\s*(?:soon|quick|immediate|fast))(?:ly)?|(?:need\s*(?:help|support|fixed|answer|reply|response)!)|(?:emergency|critical|urgen(?:t|cy)|high.?priority)/igm; - const bFoundMatchTitle = Boolean( findWordList.test( iss_title ) ); - const bFoundMatchBody = Boolean( findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const urgn_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const urgn_bFoundPRTitle = Boolean( urgn_findPRTitle.test( iss_title ) ); - - console.log( "Title Lowercase ............... " + iss_title_lc ) - console.log( "Startswith " + urgn_tag.toLowerCase( ) + "............. " + iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) ) - console.log( "Title Includes Keyword ........ " + bTriggerWordInTitle ) - console.log( "Title Includes Regex .......... " + bFoundMatchTitle ) - console.log( "Body Includes Regex ........... " + bFoundMatchBody ) - console.log( "\n" ) - - /* - - Check if issue title matches the issue label "Urgent:" - - Check if title contains word in words - */ - - // change TAG per category - if ( iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) || bTriggerWordInTitle || bFoundMatchTitle || bFoundMatchBody ) - { - - console.log( "⚠️ " + urgn_tag + " ---------------------------------------" ) - console.log( "Already starts with " + bug_tag + " ......... " + iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + feat_tag + " ..... " + iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + urgn_tag + " ...... " + iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + road_tag + " ..... " + iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - - // change LBL per category - add_labels.push( `${ urgn_lbl }` ); - - console.log( `Adding Tag ....................... ${ urgn_lbl }` ) - console.log( "\n" ) - - if ( iss_author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ iss_author }` ) - - // Rename title to contain Urgent: - // Make sure issue / pr title doesnt already contain a beginning title tag - - if ( iss_author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !urgn_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - console.log( "Renaming Title" ) - console.log( `Old Title: .................. ${ iss_title }` ) - - const title = context.payload.issue.title - let title_new = title.replace( /^\s?emergency\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?urgent\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?urgency\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?important\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?critical\s*(.*?)\b/gi, '' ); - iss_title = `${ urgn_tag } ${ title_new }`; // change TAG per category - } - - console.log( `New Title: ...................... ${ iss_title }` ) - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - core.setOutput( 'issue_title', iss_title ) - console.log( "\n\n" ) - - # # - # Labels > Roadmap - # - # Title of issue is carried over from the previous step. - # # - - - name: >- - 🏷️ ${{ env.PREFIX_ROADMAP }} › Assignment - uses: actions/github-script@v7 - id: task_issues_roadmap - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const issueLabels = await github.rest.issues.listLabelsOnIssue( - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - let add_labels = issueLabels.data.map( label => label.name ); - - let iss_title = `${{ steps.task_issues_urgent.outputs.issue_title }}` || `${ context.payload.issue.title }`; - let iss_body = `${ context.payload.issue.body }`; - - const iss_title_lc = iss_title.toLowerCase( ); - - console.log( "Road Title .................... " + iss_title ) - console.log( "Road Output ................... " + `${{ steps.task_issues_urgent.outputs.issue_title }}` ) - console.log( "Road Payload .................. " + `${ context.payload.issue.title }` ) - - /* - Tags - */ - - const bug_tag = `${{ env.PREFIX_BUG }}:`; - const bug_lbl = `${{ env.LABEL_BUG }}`; - const feat_tag = `${{ env.PREFIX_FEATURE }}:`; - const feat_lbl = `${{ env.LABEL_FEATURE }}`; - const urgn_tag = `${{ env.PREFIX_URGENT }}:`; - const urgn_lbl = `${{ env.LABEL_URGENT }}`; - const road_tag = `${{ env.PREFIX_ROADMAP }}:`; - const road_lbl = `${{ env.LABEL_ROADMAP }}`; - - /* - Roadmap - */ - - const words = [ "roadmap", "road map", "planned" ]; - const bTriggerWordInTitle = words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - Roadmap requires headers #Summary and #Proposal | #Objective - - Regex: - https://regex101.com/r/ucajBZ/1 - */ - - const findWordList = /#\s*Summary[\S\s]+#\s*(?:Proposal|Objective)[^\]]+/igm; - const bFoundMatchTitle = Boolean( findWordList.test( iss_title ) ); - const bFoundMatchBody = Boolean( findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const road_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const road_bFoundPRTitle = Boolean( road_findPRTitle.test( iss_title ) ); - - console.log( "Title Lowercase ............... " + iss_title_lc ) - console.log( "Startswith " + road_tag.toLowerCase( ) + "............ " + iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - console.log( "Title Includes Keyword ........ " + bTriggerWordInTitle ) - console.log( "Title Includes Regex .......... " + bFoundMatchTitle ) - console.log( "Body Includes Regex ........... " + bFoundMatchBody ) - console.log( "\n" ) - - /* - - Check if issue title matches the issue label "Roadmap:" - - Check if title contains word in words - */ - - // change TAG per category - if ( iss_title_lc.startsWith( road_tag.toLowerCase( ) ) || bTriggerWordInTitle || bFoundMatchTitle || bFoundMatchBody ) - { - - console.log( "⚠️ " + road_tag + " ---------------------------------------" ) - console.log( "Already starts with " + bug_tag + " ...... " + iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + feat_tag + " .. " + iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + urgn_tag + " ... " + iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) ) - console.log( "Already starts with " + road_tag + " .. " + iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - - // change LBL per category - add_labels.push( `${ road_lbl }` ); - - console.log( `Adding Tag .................... ${ road_lbl }` ) - console.log( "\n" ) - - if ( iss_author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ iss_author }` ) - - // Rename title to contain Roadmap: - // Make sure issue / pr title doesnt already contain a beginning title tag - - if ( iss_author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !road_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - console.log( "Renaming Title" ) - console.log( `Old Title: .................. ${ iss_title }` ) - - const title = context.payload.issue.title - let title_new = title.replace( /^\s?broad(.*?)\s?map\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?planned\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?broadmap\s*(.*?)\b/gi, '' ); - iss_title = `${ road_tag } ${ title_new }`; // change TAG per category - } - - console.log( `New Title: .................... ${ iss_title }` ) - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - core.setOutput( 'issue_title', iss_title ) - console.log( "\n\n" ) - - # # - # Job > Phrase Search - # - # Checks a message for certain keywords and then responds to the user as a reply / comment - # # - - job-phrase-search: - name: >- - 🏷️ Labels › Phrase Search - needs: - - job-labels-create - runs-on: ubuntu-latest - permissions: - contents: 'read' - id-token: 'write' - issues: 'write' - steps: - - # # - # [ Search Phrase ] Checkout - # # - - - name: >- - ☑️ Prepare - id: issues-labels-check-checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # [ Search Phrase ] Search - # # - - - name: >- - 👄 Search Phrases - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const fs = require( 'fs' ); - const iss_title = `${ context.payload.issue.title }`; - const iss_body = `${ context.payload.issue.body }`; - let message = [ "\n<br />\n" ] - let bHasMessage = false - - /********************************************* - Keyword > Help - **********************************************/ - - let HE_message = - ` - 💡 It appears you might need help, please check the resources below for documentation that might assist with your issue: - - [Documentation](${{github.event.repository.url}}) - - --- - - <sub>I am a bot reaching out to you with an automated response. If the above info doesn't apply to you, please ignore it.</sub> - `; - - /* - found searched word "for help" - append / prepare message for bot to send - */ - - const HEfindWordList = /^\b(?:have\s*(?:a|some)?\s*question*s?)|(?:can\s*you\s*(?:tell|help)\s*me)|(?:need\s*(?:some)?\s*(?:help|assistance|guidance))|(?:how\s*can\s*I\s*find)|(?:point\s*me\s*in\s*the\s*direction)|(?:where\s*can\s*I\s*find)|(?:where\s*(?:\N*)\s*(?:\N*)\s*find)|(?:please\s*help)|(?:where\s*\N*\s*(?:located|at))|(?:documentation)\b$/igm; - const HEbFoundMatchTitle = Boolean( HEfindWordList.test( iss_title ) ); - const HEbFoundMatchBody = Boolean( HEfindWordList.test( iss_body ) ); - - if ( HEbFoundMatchTitle || HEbFoundMatchBody ) - { - message.push ( HE_message ); - bHasMessage = true; - } - - /* - Bot has message to send - */ - - if ( bHasMessage == true ) - { - await github.rest.issues.createComment( - { - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: message.join('\n'), - } ); - } - - # # - # Job > Add Assignees - # # - - job-assign-assignees: - name: >- - ✍️ Issue › Assignees - runs-on: ubuntu-latest - needs: [ job-assign-labels ] - if: | - always() - && contains( needs.*.result, 'success' ) - && !contains( needs.*.result, 'failure' ) - permissions: - contents: write - steps: - - # # - # [ Assignees] Assign - # # - - - name: >- - ✍️ Set Assignees - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const assignees = [ `${{ github.repository_owner }}` ]; - - if ( assignees.length > 0 ) - { - try - { - await github.rest.issues.addAssignees( - { - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - assignees - }); - } - catch ( error ) - { - core.setFailed( error.message ); - } - } diff --git a/.github/workflows/issues-scan.yml b/.github/workflows/issues-scan.yml index 2121a3cf..95e5a419 100644 --- a/.github/workflows/issues-scan.yml +++ b/.github/workflows/issues-scan.yml @@ -1,9 +1,10 @@ # # # @type github workflow # @desc pull request autoscan -# scans all of the files related to a particular pull request -# if the code in the files being submitted contains code that is forbidden, -# a report is generated and posted as a comment in the PR. +# scans all of the files related to a particular pull request +# if the code in the files being submitted contains code that is forbidden, +# a report is generated and posted as a comment in the PR. +# sends notifications to discord using webhooks # @author Aetherinox # @url https://github.com/Aetherinox # # @@ -18,8 +19,7 @@ run-name: "🎫 Issues › Scan" on: pull_request_target: branches: - - main - - master + - alpine-base # # # environment variables @@ -38,9 +38,7 @@ env: LABEL_TYPE_DEPENDENCY: Type ◦ Dependency LABEL_TYPE_GITACTION: Type ◦ Git Action - BOT_NAME_1: AdminServ - BOT_NAME_2: AdminServX - BOT_NAME_3: EuropaServ + BOT_NAME_1: EuropaServ BOT_NAME_DEPENDABOT: dependabot[bot] LABELS_JSON: | @@ -124,108 +122,121 @@ jobs: pull-requests: read steps: + # # + # Cleanup › Set Env Variables + # # + + - name: >- + 🕛 Get Timestamp + id: task_autocheck_set_timestamp + run: | + echo "NOW=$(date +'%m-%d-%Y %H:%M:%S')" >> $GITHUB_ENV + echo "NOW_SHORT=$(date +'%m-%d-%Y')" >> $GITHUB_ENV + echo "NOW_LONG=$(date +'%m-%d-%Y %H:%M')" >> $GITHUB_ENV + echo "NOW_DOCKER_LABEL=$(date +'%Y%m%d')" >> $GITHUB_ENV + # # # action needed if using 'pull_request' and 'issue_comment' # to get the pull request, you would normally use ${{ github.event.number }} # however this isnt available for 'issue_comment' # # - - name: >- - 🏷️ Verify Existing Labels - id: task_autocheck_labels_verify - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }} - script: | - const labels = JSON.parse( process.env.LABELS_JSON ); - for ( const label of labels ) - { - try - { - await github.rest.issues.createLabel( - { - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - description: label.description || '', - color: label.color - }); - } - catch ( err ) - { - if ( err.status === 422 ) - { - console.log( `Label '${label.name}' already exists. Skipping.` ); - } - else - { - console.error( `Error creating label '${label.name}': ${err}` ); - } - } - } + - name: >- + 🏷️ Verify Existing Labels + id: task_autocheck_labels_verify + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }} + script: | + const labels = JSON.parse( process.env.LABELS_JSON ); + for ( const label of labels ) + { + try + { + await github.rest.issues.createLabel( + { + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + description: label.description || '', + color: label.color + }); + } + catch ( err ) + { + if ( err.status === 422 ) + { + console.log( `Label '${label.name}' already exists. Skipping.` ); + } + else + { + console.error( `Error creating label '${label.name}': ${err}` ); + } + } + } # # # set issue number # # - - name: >- - #️⃣ Issue number › Set - uses: actions/github-script@v7 - id: task_autocheck_issue_num_set - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }} - script: | - if ( context.issue.number ) - { - // Return issue number if present - return context.issue.number; - } - else - { - // Otherwise return issue number from commit - return ( - await github.rest.repos.listPullRequestsAssociatedWithCommit( - { - commit_sha: context.sha, - owner: context.repo.owner, - repo: context.repo.repo, - }) - ).data[ 0 ].number; - } - result-encoding: string + - name: >- + #️⃣ Issue number › Set + uses: actions/github-script@v7 + id: task_autocheck_issue_num_set + with: + github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }} + script: | + if ( context.issue.number ) + { + // Return issue number if present + return context.issue.number; + } + else + { + // Otherwise return issue number from commit + return ( + await github.rest.repos.listPullRequestsAssociatedWithCommit( + { + commit_sha: context.sha, + owner: context.repo.owner, + repo: context.repo.repo, + }) + ).data[ 0 ].number; + } + result-encoding: string # # # print issue number # # - - name: >- - #️⃣ Issue number › Print - id: task_autocheck_issue_num_get - run: | - echo '${{ steps.task_autocheck_issue_num_set.outputs.result }}' + - name: >- + #️⃣ Issue number › Print + id: task_autocheck_issue_num_get + run: | + echo '${{ steps.task_autocheck_issue_num_set.outputs.result }}' # # # checkout # # - - name: >- - ☑️ Checkout - id: task_autoscan_checkout - uses: actions/checkout@v4 - if: | - ( github.event_name == 'pull_request_target' ) || ( github.event_name == 'pull_request' ) || ( github.event_name == 'issue_comment' && contains( github.event.comment.html_url, '/pull/' ) && contains( github.event.comment.body, '/rescan' ) ) - with: - fetch-depth: 0 - ref: "refs/pull/${{ steps.task_autocheck_issue_num_set.outputs.result }}/merge" + - name: >- + ☑️ Checkout + id: task_autoscan_checkout + uses: actions/checkout@v4 + if: | + ( github.event_name == 'pull_request_target' ) || ( github.event_name == 'pull_request' ) || ( github.event_name == 'issue_comment' && contains( github.event.comment.html_url, '/pull/' ) && contains( github.event.comment.body, '/rescan' ) ) + with: + fetch-depth: 0 + ref: "refs/pull/${{ steps.task_autocheck_issue_num_set.outputs.result }}/merge" # # # nodejs # # - - name: >- - ⚙️ Setup Node - id: task_autocheck_nodejs - uses: actions/setup-node@v4 + - name: >- + ⚙️ Setup Node + id: task_autocheck_nodejs + uses: actions/setup-node@v4 # # # get list of changed files @@ -236,553 +247,665 @@ jobs: # GitHub action. # # - - name: >- - 📄 Get changed files - id: task_autocheck_changed_files_get - uses: tj-actions/changed-files@v45 - with: - separator: "," + - name: >- + 📄 Get changed files + id: task_autocheck_changed_files_get + uses: tj-actions/changed-files@v45 + with: + separator: "," # # # list of changed files # # - - name: >- - 📄 List all added files - id: task_autocheck_added_files_get - run: | - for file in ${CHANGED_FILES}; do - echo "$file was changed" - done - env: - ADDED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.added_files }} - MODIFIED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.modified_files }} - CHANGED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.all_changed_files }} - COUNT_ADDED: ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }} - COUNT_MODIFIED: ${{ steps.task_autocheck_changed_files_get.outputs.modified_files_count }} - COUNT_DELETED: ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} - COUNT_RENAMED: ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }} - COUNT_COPIED: ${{ steps.task_autocheck_changed_files_get.outputs.copied_files_count }} + - name: >- + 📄 List all added files + id: task_autocheck_added_files_get + run: | + for file in ${CHANGED_FILES}; do + echo "$file was changed" + done + env: + ADDED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.added_files }} + MODIFIED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.modified_files }} + CHANGED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.all_changed_files }} + COUNT_ADDED: ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }} + COUNT_MODIFIED: ${{ steps.task_autocheck_changed_files_get.outputs.modified_files_count }} + COUNT_DELETED: ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} + COUNT_RENAMED: ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }} + COUNT_COPIED: ${{ steps.task_autocheck_changed_files_get.outputs.copied_files_count }} # # # List directories # # - - name: >- - 📂 List Directories - id: task_autocheck_dirs_list - run: | - ls + - name: >- + 📂 List Directories + id: task_autocheck_dirs_list + run: | + ls # # # Run autocheck # # - - name: >- - ☑️ Run Autocheck - id: task_autocheck_run - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }} - script: | - const fs = require( 'fs' ); - const escape_html = ( unsafe ) => unsafe.replace( /&/g, '&' ).replace( /</g, '<' ).replace( />/g, '>' ).replace( /"/g, '"' ).replace( /'/g, ''' ); - const labels = []; - - const files_List = `${{ steps.task_autocheck_changed_files_get.outputs.all_changed_files }}` || '' - const files_Array = files_List.split(',') - const branch_ref = `${ context.payload.pull_request.head.ref }` - - let message = [ "\n<br />\n" ] - message.push ( "## Automatic Self-Check - #" + context.issue.number + "\n" ); - message.push ( `The details of our automated scan for your pull request are listed below. If our scan detected errors, they must be corrected before this pull request will be advanced to the review stage:\n` ); - message.push ( "\n<br />\n\n---\n\n<br />\n\n" ); - message.push ( "### About\nThis pull request includes the following information:" ); - - let bHasError = false; - let bHasWarning = false; - - let date = new Date( `${ context.payload.pull_request.created_at }` ); - date.toISOString( ) - - const actor = '${{ github.actor }}'; - - const dateTimeformat = ( date ) => - { - let month = date.getMonth( ) + 1; - month = month.toString( ).padStart( 2, '0' ); - let day = date.getDate( ).toString( ).padStart( 2, '0' ); - let year = date.getFullYear( ).toString( ).padStart( 2, '0' ); - - let hours = date.getHours(); - let minutes = date.getMinutes(); - let x = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; - minutes = minutes.toString( ).padStart( 2, '0' ); - - let mergeTime = month + '.' + day + '.' + year + ' ' + hours + ':' + minutes + ' ' + x; - - return mergeTime; - } - - let date_created = dateTimeformat( date ) + " UTC"; - - /* - context.payload.pull_request.base.repo.owner.login - */ - - let md_table = - ` - | Category | Value | - | --- | --- | - | Title | [ ` + context.payload.pull_request.title + ` ](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/pull/` + context.payload.pull_request.number + `) | - | Created | [ ` + date_created + ` ](https://worldtimebuddy.com) | - | ID | ` + context.payload.pull_request.html_url + ` | - | Author | [ ` + context.payload.pull_request.user.login + ` ](https://github.com/` + context.repo.owner + `/) | - | Repo | [ ` + context.repo.repo + ` ](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `) | - | Branch | [ ` + context.payload.pull_request.head.ref + `](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/tree/` + context.payload.pull_request.head.ref + `) ⇁ [ ` + context.payload.pull_request.base.ref + `](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/tree/` + context.payload.pull_request.base.ref + `) | - | Added Files | ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }} | - | Modified Files | ${{ steps.task_autocheck_changed_files_get.outputs.all_modified_files_count }} | - | Renamed Files | ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }} | - | Copied Files | ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} | - | Deleted Files | ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} | - `; - - message.push ( md_table ); - - let error_Generic = "\n" + - "- `MyPlugin`\n" + - "- `MyPluginSettings`\n" + - "- `SampleSettings`\n" + - "- `SampleSettingTab`\n" + - "- `SampleModal`\n" - - let warn_BadWords = "\n" + - "- `General`\n" + - "- `Settings`\n" - - /* - Loop files - */ - - const files_skipped = []; - - /* - List of files to skip check - Entries are CASE sensitive - For folders, append / at the end of the parent directory - */ - - const type_dependency = - [ - "dependabot/npm_and_yarn" - ]; - - const type_gitaction = - [ - "dependabot/github_actions" - ]; - - const files_skipList = - [ - ".github", - ".gitea", - ".gitignore", - "LICENSE", - ".md", - ".yml", - "plugins.json", - "package.json", - "package-lock.json", - "rollup.config.js", - "index.js", - "gistr.js", - "Docs/", - "tests/" - ]; - - for ( const file of files_Array ) - { - - const errors = []; - const addError = ( error ) => - { - errors.push ( `:x: ${error}` ); - console.log ( 'Found Issues: ' + error ); - - bHasError = true; - }; - - const warnings = []; - const addWarning = ( warning ) => - { - warnings.push ( `:warning: ${warning}` ); - console.log ( 'Found Warnings: ' + warning ); - - bHasWarning = true; - } - - /* - Regex Searches - */ - - const file_current = file; - const filesData = fs.readFileSync( file_current, 'utf8' ); - const bContainsStyle = /([A-Za-z]+\.style\.[A-Za-z]+)/gi.test( filesData ); - const bFuncFetch = /(fetch)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData ); - const bVar = /^(?:var|)\s(\w+)\s*=\s*/gm.test( filesData ); - const bLookBehind = /\(\?<[=!].*?\)/gmi.test( filesData ); - const bMarkdownHtmlNode = /new\s+NodeHtmlMarkdown/gmi.test( filesData ); - const bAsTFile = /as\s+TFile/g.test( filesData ); - const bAsTFolder = /as\s+TFolder/g.test( filesData ); - const bAsAny = /\((.*? as Any\s*)\)/gi.test( filesData ); - const bInnerHTML = /^\s?.*[a-zA-Z0-9_]+\.innerHTML*\s?.*$/gm.test( filesData ); - const bOuterHTML = /^\s?.*[a-zA-Z0-9_]+\.outerHTML*\s?.*$/gm.test( filesData ); - // const bFuncConsoleLog = /(console.log)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData ); - const bFuncSetTimeout = /(setTimeout)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData ); - const bFuncFS_Chk1 = /(require)\s?\((\s?(?:'|")fs(?:'|"))\s?\)?/gim.test( filesData ); - const bFuncFS_Chk2 = /from\s+(?:'|")fs(?:'|")\s?/gim.test( filesData ); - const bFuncFS_ExistsSync = /(fs.existsSync)\((.*)\)(\[([^\]]*)\])?/gm.test( filesData ); - const bFuncFS_MkdirSync = /(fs.mkdirSync)\((.*)\)(\[([^\]]*)\])?/gm.test( filesData ); - const bFoundBadWord = /(?:'|").*(Settings|General).*(?:'|")?/gmi.test( filesData ); - const bContainsGeneric = /(?:^|(?<= ))(MyPlugin|MyPluginSettings|SampleSettings|SampleSettingTab|SampleModal|Sample Plugin|my-plugin)(?:(?= )|$)/gim.test( filesData ); - const check_depGetUnpinnedLeaf = "app.workspace.getUnpinnedLeaf" - - const bFileSkip = files_skipList.some( s => s.includes( file_current ) || file_current.includes( s ) ); - - if ( bFileSkip == true ) - { - files_skipped.push( file_current ); - continue; - } - - /* - Header - */ - - message.push ( "\n<br />\n\n---\n\n<br />\n" ); - message.push ( "### 📄 " + file_current + "\n" ); - message = message.concat( warnings ); - - /* - Skip File - - all contents in the array below will be skipped. - - E.g: any file which resides in the .github folder will be skipped. - any file which ends in .yml will be skipped. - */ - - /* - ( Deprecated ) app.workspace.getUnpinnedLeaf - - @usage : obsidian.md - */ - - /* - if ( filesData.toLowerCase( ).includes( check_depGetUnpinnedLeaf.toLowerCase( ) ) ) - { - addError( "This function is deprecated, use `this.app.workspace.getLeaf( false )` instead" ); - } - */ - - /* - Using inline style - */ - - if ( bContainsStyle == true ) - { - addError( "Avoid assigning `inline styles` via JavaScript or in HTML. Move these styles to CSS so that they are adaptable by themes and other plugins." ); - } - - /* - Using fetch - */ - - if ( bFuncFetch == true ) - { - addError( "Do not handle http data with `fetch( )`. Use the Obsidian API -> `requestUrl` method instead, which will make sure that network requests work on every platform." ); - } - - /* - Using var - */ - - if ( bVar == true ) - { - addError( "Change all instances of `var` to **const** or **let**. var has function-level scope, and leads to bugs." ); - } - - /* - Using lookbehind - */ - - if ( bLookBehind == true ) - { - addError( "Lookbehinds are not supported in iOS < 16.4" ); - } - - /* - Using HTML Node - */ - - if ( bMarkdownHtmlNode == true ) - { - addError( "Do not use `NodeHtmlMarkdown`. Use Obsidian API -> `htmlToMarkdown` instead." ); - } - - /* - As TFile - */ - - if ( bAsTFile == true ) - { - addError( "Do not cast `as TFile`, use `instanceof` instead to check if the item is actually a file / folder" ); - } - - /* - As TFolder - */ - - if ( bAsTFolder == true ) - { - addError( "Do not cast `as TFolder`, use `instanceof` instead to check if the item is actually a file / folder" ); - } - - /* - Casting to Any - */ - - if ( bAsAny == true ) - { - addError( "Do not cast to `Any`" ); - } - - /* - innerHTML - */ - - if ( bInnerHTML == true ) - { - addError( `Using \`innerHTML\` is a security risk.` ); - } - - /* - outerHTML - */ - - if ( bOuterHTML == true ) - { - addError( `Using \`outerHTML\` is a security risk.` ); - } - - /* - setTimeout - */ - - if ( bFuncSetTimeout == true ) - { - addError( "Do not utilize `setTimeout`, utilize Obsidian API -> `sleep`. E.g: `await sleep( X )`" ); - } - - /* - require("fs") - */ - - if ( bFuncFS_Chk1 == true || bFuncFS_Chk2 == true ) - { - addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile" ); - } - - /* - require("fs") / fs.existsSync - */ - - if ( bFuncFS_ExistsSync == true ) - { - addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile." ); - } - - /* - require("fs") / fs.mkdirSync - */ - - if ( bFuncFS_MkdirSync == true ) - { - addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile." ); - } - - /* - Generic Calls - */ - - if ( bContainsGeneric == true ) - { - addError( "Rename sample classes to something that makes sense. You are not allowed to have names such as: " + error_Generic ); - } - - /* - console.log found - */ - - /* - if ( bFuncConsoleLog == true ) - { - addWarning( "Avoid unnecessary logging or ensure logging only occurs in development environment." ); - } - */ - - /* - Bad words found - */ - - if ( bFoundBadWord == true && file != "package.json" && file != "manifest.json" ) - { - addWarning( "A restricted word was found in your code. Generic words are not allowed in strings such as: " + warn_BadWords ); - } - - if ( errors.length > 0 || warnings.length > 0 ) - { - - /* - Errors - */ - - if ( errors.length > 0 ) - { - message.push ( "\n\n\n> [!CAUTION]\n> Errors must be fixed prior to a pull request being reviewed and accepted.<br />The file `" + file + "` contains the following errors:\n\n<br>\n\n" ); - message = message.concat( errors ); - } - - /* - Warnings - */ - - if ( warnings.length > 0 ) - { - if ( errors.length > 0 ) - { - message.push ( "\n<br />\n<br />\n" ) - } - message.push ( "\n\n\n> [!WARNING]\n> Warnings are suggestions that do not require fixing, but are recommended before this pull request is reviewed and accepted.<br />The file `" + file + "` contains the following warnings:\n\n<br>\n\n" ); - message = message.concat( warnings ); - } - } - else - { - message.push ( "\n\n\n> [!NOTE]\n> The file `" + file + "` contains no errors\n\n<br>\n\n" ); - } - } - - if ( files_skipped.length > 0 ) - { - message.push ( "\n<br />\n\n---\n<br />\n" ); - message.push ( "### ❌ Skipped Files\n" ); - - message.push ( "\n\n\n> [!TIP]\n> The following file(s) have been skipped:\n\n<br>\n\n" ); - - for ( const file_skipped of files_skipped ) - { - message.push ( "- " + file_skipped ); - } - } - - /* - footer - */ - - message.push ( "\n<br />\n\n---\n<br />\n" ); - message.push ( `<sup>This check was done automatically. Do <b>NOT</b> open a new PR for re-validation. Instead, to trigger this check again, make a change to your PR and wait a few minutes, or close and re-open it.</sup>` ); - - /* - Has Errors - */ - - if ( bHasError == true ) - { - labels.push( "${{ env.LABEL_CHECK_STATUS_FAILED }}" ); - core.setFailed( "Pull Request Failed Autocheck: " + context.issue.number + ": " + context.payload.pull_request.title + "." ); - } - - /* - No Errors - */ - - if ( bHasError == false ) - { - - /* - change pr title - */ - - const pr_title = `${ context.payload.pull_request.title }`; - const pr_title_append = `PR ${ context.issue.number }:`; - - if ( !pr_title.startsWith( pr_title_append ) ) - { - await github.rest.pulls.update( - { - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - title: `${ pr_title_append } ${ context.payload.pull_request.title }` - } ); - } - - if ( !context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_CHANGES_REQ }}" ).length > 0 ) - labels.push( "${{ env.LABEL_CHECK_REVIEW_READY }}" ); - } - - /* - Determine Labels - */ - - const bGitaction = type_gitaction.some( s => s.includes( branch_ref ) || branch_ref.includes( s ) ); - const bDependency = type_dependency.some( s => s.includes( branch_ref ) || branch_ref.includes( s ) ); - - if ( actor == "${{ env.BOT_NAME_DEPENDABOT }}" && bDependency ) - labels.push( "${{ env.LABEL_TYPE_DEPENDENCY }}" ); - else if ( actor == "${{ env.BOT_NAME_DEPENDABOT }}" && bGitaction ) - labels.push( "${{ env.LABEL_TYPE_GITACTION }}" ); - - if ( context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_CHANGES_REQ }}" ).length > 0 ) - labels.push( "${{ env.LABEL_CHECK_CHANGES_REQ }}" ); - - if (context.payload.pull_request.labels.filter(label => label.name === "${{ env.LABEL_CHECK_REBASE_REQ }}" ).length > 0 ) - labels.push( "${{ env.LABEL_CHECK_REBASE_REQ }}" ); - - if ( context.payload.pull_request.labels.filter(label => label.name === "${{ env.LABEL_CHECK_SECURITY_ERR }}" ).length > 0 ) - labels.push( "${{ env.LABEL_CHECK_SECURITY_ERR }}" ); - - if (context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_STATUS_CHGMADE }}" ).length > 0 ) - labels.push( "${{ env.LABEL_CHECK_STATUS_CHGMADE }}" ); - - if ( context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_SCAN_SKIPPED }}" ).length > 0 ) - labels.push( "${{ env.LABEL_CHECK_SCAN_SKIPPED }}" ); - - labels.push( "${{ env.LABEL_TYPE_PR }}" ); - - /* - Set Label - */ - - await github.rest.issues.setLabels( - { - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels, - } ); - - /* - Create Comment - */ - - await github.rest.issues.createComment( - { - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: message.join('\n'), - } ); + - name: >- + ☑️ Run Autocheck + id: task_autocheck_run + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} + script: | + const fs = require( 'fs' ); + const escape_html = ( unsafe ) => unsafe.replace( /&/g, '&' ).replace( /</g, '<' ).replace( />/g, '>' ).replace( /"/g, '"' ).replace( /'/g, ''' ); + const labels = []; + + const files_List = `${{ steps.task_autocheck_changed_files_get.outputs.all_changed_files }}` || '' + const files_Array = files_List.split(',') + const branch_ref = `${ context.payload.pull_request.head.ref }` + + let message = [ "\n<br />\n" ] + message.push ( "## Automatic Self-Check - #" + context.issue.number + "\n" ); + message.push ( `The details of our automated scan for your pull request are listed below. If our scan detected errors, they must be corrected before this pull request will be advanced to the review stage:\n` ); + message.push ( "\n<br />\n\n---\n\n<br />\n\n" ); + message.push ( "### About\nThis pull request includes the following information:" ); + + let bHasError = false; + let bHasWarning = false; + + let date = new Date( `${ context.payload.pull_request.created_at }` ); + date.toISOString( ) + + const actor = '${{ github.actor }}'; + + const dateTimeformat = ( date ) => + { + let month = date.getMonth( ) + 1; + month = month.toString( ).padStart( 2, '0' ); + let day = date.getDate( ).toString( ).padStart( 2, '0' ); + let year = date.getFullYear( ).toString( ).padStart( 2, '0' ); + + let hours = date.getHours(); + let minutes = date.getMinutes(); + let x = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; + minutes = minutes.toString( ).padStart( 2, '0' ); + + let mergeTime = month + '.' + day + '.' + year + ' ' + hours + ':' + minutes + ' ' + x; + + return mergeTime; + } + + let date_created = dateTimeformat( date ) + " UTC"; + + /* + context.payload.pull_request.base.repo.owner.login + */ + + let md_table = + ` + | Category | Value | + | --- | --- | + | Title | [ ` + context.payload.pull_request.title + ` ](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/pull/` + context.payload.pull_request.number + `) | + | Created | [ ` + date_created + ` ](https://worldtimebuddy.com) | + | ID | ` + context.payload.pull_request.html_url + ` | + | Author | [ ` + context.payload.pull_request.user.login + ` ](https://github.com/` + context.repo.owner + `/) | + | Repo | [ ` + context.repo.repo + ` ](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `) | + | Branch | [ ` + context.payload.pull_request.head.ref + `](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/tree/` + context.payload.pull_request.head.ref + `) ⇁ [ ` + context.payload.pull_request.base.ref + `](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/tree/` + context.payload.pull_request.base.ref + `) | + | Added Files | ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }} | + | Modified Files | ${{ steps.task_autocheck_changed_files_get.outputs.all_modified_files_count }} | + | Renamed Files | ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }} | + | Copied Files | ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} | + | Deleted Files | ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} | + `; + + message.push ( md_table ); + + let error_Generic = "\n" + + "- `MyPlugin`\n" + + "- `MyPluginSettings`\n" + + "- `SampleSettings`\n" + + "- `SampleSettingTab`\n" + + "- `SampleModal`\n" + + let warn_BadWords = "\n" + + "- `General`\n" + + "- `Settings`\n" + + /* + Loop files + */ + + const files_skipped = []; + + /* + List of files to skip check + Entries are CASE sensitive + For folders, append / at the end of the parent directory + */ + + const type_dependency = + [ + "dependabot/npm_and_yarn" + ]; + + const type_gitaction = + [ + "dependabot/github_actions" + ]; + + const files_skipList = + [ + ".github", + ".gitea", + ".gitignore", + "LICENSE", + ".md", + ".yml", + "plugins.json", + "package.json", + "package-lock.json", + "rollup.config.js", + "index.js", + "gistr.js", + "Docs/", + "tests/" + ]; + + for ( const file of files_Array ) + { + + const errors = []; + const addError = ( error ) => + { + errors.push ( `:x: ${error}` ); + console.log ( 'Found Issues: ' + error ); + + bHasError = true; + }; + + const warnings = []; + const addWarning = ( warning ) => + { + warnings.push ( `:warning: ${warning}` ); + console.log ( 'Found Warnings: ' + warning ); + + bHasWarning = true; + } + + /* + Regex Searches + */ + + const file_current = file; + const filesData = fs.readFileSync( file_current, 'utf8' ); + const bContainsStyle = /([A-Za-z]+\.style\.[A-Za-z]+)/gi.test( filesData ); + const bFuncFetch = /(fetch)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData ); + const bVar = /^(?:var|)\s(\w+)\s*=\s*/gm.test( filesData ); + const bLookBehind = /\(\?<[=!].*?\)/gmi.test( filesData ); + const bMarkdownHtmlNode = /new\s+NodeHtmlMarkdown/gmi.test( filesData ); + const bAsTFile = /as\s+TFile/g.test( filesData ); + const bAsTFolder = /as\s+TFolder/g.test( filesData ); + const bAsAny = /\((.*? as Any\s*)\)/gi.test( filesData ); + const bInnerHTML = /^\s?.*[a-zA-Z0-9_]+\.innerHTML*\s?.*$/gm.test( filesData ); + const bOuterHTML = /^\s?.*[a-zA-Z0-9_]+\.outerHTML*\s?.*$/gm.test( filesData ); + // const bFuncConsoleLog = /(console.log)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData ); + const bFuncSetTimeout = /(setTimeout)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData ); + const bFuncFS_Chk1 = /(require)\s?\((\s?(?:'|")fs(?:'|"))\s?\)?/gim.test( filesData ); + const bFuncFS_Chk2 = /from\s+(?:'|")fs(?:'|")\s?/gim.test( filesData ); + const bFuncFS_ExistsSync = /(fs.existsSync)\((.*)\)(\[([^\]]*)\])?/gm.test( filesData ); + const bFuncFS_MkdirSync = /(fs.mkdirSync)\((.*)\)(\[([^\]]*)\])?/gm.test( filesData ); + const bFoundBadWord = /(?:'|").*(Settings|General).*(?:'|")?/gmi.test( filesData ); + const bContainsGeneric = /(?:^|(?<= ))(MyPlugin|MyPluginSettings|SampleSettings|SampleSettingTab|SampleModal|Sample Plugin|my-plugin)(?:(?= )|$)/gim.test( filesData ); + const check_depGetUnpinnedLeaf = "app.workspace.getUnpinnedLeaf" + + const bFileSkip = files_skipList.some( s => s.includes( file_current ) || file_current.includes( s ) ); + + if ( bFileSkip == true ) + { + files_skipped.push( file_current ); + continue; + } + + /* + Header + */ + + message.push ( "\n<br />\n\n---\n\n<br />\n" ); + message.push ( "### 📄 " + file_current + "\n" ); + message = message.concat( warnings ); + + /* + Skip File + + all contents in the array below will be skipped. + + E.g: any file which resides in the .github folder will be skipped. + any file which ends in .yml will be skipped. + */ + + /* + ( Deprecated ) app.workspace.getUnpinnedLeaf + + @usage : obsidian.md + */ + + /* + if ( filesData.toLowerCase( ).includes( check_depGetUnpinnedLeaf.toLowerCase( ) ) ) + { + addError( "This function is deprecated, use `this.app.workspace.getLeaf( false )` instead" ); + } + */ + + /* + Using inline style + */ + + if ( bContainsStyle == true ) + { + addError( "Avoid assigning `inline styles` via JavaScript or in HTML. Move these styles to CSS so that they are adaptable by themes and other plugins." ); + } + + /* + Using fetch + */ + + if ( bFuncFetch == true ) + { + addError( "Do not handle http data with `fetch( )`. Use the Obsidian API -> `requestUrl` method instead, which will make sure that network requests work on every platform." ); + } + + /* + Using var + */ + + if ( bVar == true ) + { + addError( "Change all instances of `var` to **const** or **let**. var has function-level scope, and leads to bugs." ); + } + + /* + Using lookbehind + */ + + if ( bLookBehind == true ) + { + addError( "Lookbehinds are not supported in iOS < 16.4" ); + } + + /* + Using HTML Node + */ + + if ( bMarkdownHtmlNode == true ) + { + addError( "Do not use `NodeHtmlMarkdown`. Use Obsidian API -> `htmlToMarkdown` instead." ); + } + + /* + As TFile + */ + + if ( bAsTFile == true ) + { + addError( "Do not cast `as TFile`, use `instanceof` instead to check if the item is actually a file / folder" ); + } + + /* + As TFolder + */ + + if ( bAsTFolder == true ) + { + addError( "Do not cast `as TFolder`, use `instanceof` instead to check if the item is actually a file / folder" ); + } + + /* + Casting to Any + */ + + if ( bAsAny == true ) + { + addError( "Do not cast to `Any`" ); + } + + /* + innerHTML + */ + + if ( bInnerHTML == true ) + { + addError( `Using \`innerHTML\` is a security risk.` ); + } + + /* + outerHTML + */ + + if ( bOuterHTML == true ) + { + addError( `Using \`outerHTML\` is a security risk.` ); + } + + /* + setTimeout + */ + + if ( bFuncSetTimeout == true ) + { + addError( "Do not utilize `setTimeout`, utilize Obsidian API -> `sleep`. E.g: `await sleep( X )`" ); + } + + /* + require("fs") + */ + + if ( bFuncFS_Chk1 == true || bFuncFS_Chk2 == true ) + { + addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile" ); + } + + /* + require("fs") / fs.existsSync + */ + + if ( bFuncFS_ExistsSync == true ) + { + addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile." ); + } + + /* + require("fs") / fs.mkdirSync + */ + + if ( bFuncFS_MkdirSync == true ) + { + addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile." ); + } + + /* + Generic Calls + */ + + if ( bContainsGeneric == true ) + { + addError( "Rename sample classes to something that makes sense. You are not allowed to have names such as: " + error_Generic ); + } + + /* + console.log found + */ + + /* + if ( bFuncConsoleLog == true ) + { + addWarning( "Avoid unnecessary logging or ensure logging only occurs in development environment." ); + } + */ + + /* + Bad words found + */ + + if ( bFoundBadWord == true && file != "package.json" && file != "manifest.json" ) + { + addWarning( "A restricted word was found in your code. Generic words are not allowed in strings such as: " + warn_BadWords ); + } + + if ( errors.length > 0 || warnings.length > 0 ) + { + + /* + Errors + */ + + if ( errors.length > 0 ) + { + message.push ( "\n\n\n> [!CAUTION]\n> Errors must be fixed prior to a pull request being reviewed and accepted.<br />The file `" + file + "` contains the following errors:\n\n<br>\n\n" ); + message = message.concat( errors ); + } + + /* + Warnings + */ + + if ( warnings.length > 0 ) + { + if ( errors.length > 0 ) + { + message.push ( "\n<br />\n<br />\n" ) + } + message.push ( "\n\n\n> [!WARNING]\n> Warnings are suggestions that do not require fixing, but are recommended before this pull request is reviewed and accepted.<br />The file `" + file + "` contains the following warnings:\n\n<br>\n\n" ); + message = message.concat( warnings ); + } + } + else + { + message.push ( "\n\n\n> [!NOTE]\n> The file `" + file + "` contains no errors\n\n<br>\n\n" ); + } + } + + if ( files_skipped.length > 0 ) + { + message.push ( "\n<br />\n\n---\n<br />\n" ); + message.push ( "### ❌ Skipped Files\n" ); + + message.push ( "\n\n\n> [!TIP]\n> The following file(s) have been skipped:\n\n<br>\n\n" ); + + for ( const file_skipped of files_skipped ) + { + message.push ( "- " + file_skipped ); + } + } + + /* + footer + */ + + message.push ( "\n<br />\n\n---\n<br />\n" ); + message.push ( `<sup>This check was done automatically. Do <b>NOT</b> open a new PR for re-validation. Instead, to trigger this check again, make a change to your PR and wait a few minutes, or close and re-open it.</sup>` ); + + /* + Has Errors + */ + + if ( bHasError == true ) + { + labels.push( "${{ env.LABEL_CHECK_STATUS_FAILED }}" ); + core.setFailed( "Pull Request Failed Autocheck: " + context.issue.number + ": " + context.payload.pull_request.title + "." ); + } + + /* + No Errors + */ + + if ( bHasError == false ) + { + + /* + change pr title + */ + + const pr_title = `${ context.payload.pull_request.title }`; + const pr_title_append = `PR ${ context.issue.number }:`; + + if ( !pr_title.startsWith( pr_title_append ) ) + { + await github.rest.pulls.update( + { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + title: `${ pr_title_append } ${ context.payload.pull_request.title }` + } ); + } + + if ( !context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_CHANGES_REQ }}" ).length > 0 ) + labels.push( "${{ env.LABEL_CHECK_REVIEW_READY }}" ); + } + + /* + Determine Labels + */ + + const bGitaction = type_gitaction.some( s => s.includes( branch_ref ) || branch_ref.includes( s ) ); + const bDependency = type_dependency.some( s => s.includes( branch_ref ) || branch_ref.includes( s ) ); + + if ( actor == "${{ env.BOT_NAME_DEPENDABOT }}" && bDependency ) + labels.push( "${{ env.LABEL_TYPE_DEPENDENCY }}" ); + else if ( actor == "${{ env.BOT_NAME_DEPENDABOT }}" && bGitaction ) + labels.push( "${{ env.LABEL_TYPE_GITACTION }}" ); + + if ( context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_CHANGES_REQ }}" ).length > 0 ) + labels.push( "${{ env.LABEL_CHECK_CHANGES_REQ }}" ); + + if (context.payload.pull_request.labels.filter(label => label.name === "${{ env.LABEL_CHECK_REBASE_REQ }}" ).length > 0 ) + labels.push( "${{ env.LABEL_CHECK_REBASE_REQ }}" ); + + if ( context.payload.pull_request.labels.filter(label => label.name === "${{ env.LABEL_CHECK_SECURITY_ERR }}" ).length > 0 ) + labels.push( "${{ env.LABEL_CHECK_SECURITY_ERR }}" ); + + if (context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_STATUS_CHGMADE }}" ).length > 0 ) + labels.push( "${{ env.LABEL_CHECK_STATUS_CHGMADE }}" ); + + if ( context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_SCAN_SKIPPED }}" ).length > 0 ) + labels.push( "${{ env.LABEL_CHECK_SCAN_SKIPPED }}" ); + + labels.push( "${{ env.LABEL_TYPE_PR }}" ); + + /* + Set Label + */ + + await github.rest.issues.setLabels( + { + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels, + } ); + + /* + Create Comment + */ + + await github.rest.issues.createComment( + { + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message.join('\n'), + } ); + + # # + # Autoscan › Get Weekly Commits + # # + + - name: >- + 🕛 Get Weekly Commit List + id: task_autocheck_set_weekly_commit_list + run: | + echo 'WEEKLY_COMMITS<<EOF' >> $GITHUB_ENV + git log --format="[\`%h\`](${{ github.server_url }}/${{ github.repository }}/commit/%H) %s - %an" --since=7.days >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + + # # + # Autoscan › Notify Github › Success + # # + + - name: >- + 🔔 Send Discord Webhook Message (Success) + id: task_autocheck_notify_discord_success + uses: tsickert/discord-webhook@v6.0.0 + if: success() + with: + username: 'Io' + avatar-url: 'https://i.imgur.com/8BVDkla.jpg' + webhook-url: ${{ secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_WORKFLOWS }} + embed-title: "🎫 **Issues › Scan Workflow Ran**" + embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + embed-thumbnail-url: 'https://cdn.pixabay.com/photo/2022/01/30/13/33/github-6980894_960_720.png' + embed-description: | + ## 🎫 Issues › Scan ${{ job.status == 'success' && '✅' || '❌' }} + + **${{ job.status == 'success' && '✅ Success' || '❌ Failure' }}** › Your container just ran the `Issues › Scan` workflow. Every time this workflow is ran, your list of pull requests will be scanned to determine what files have been changed. It will scan each modified file and see if the code conforms with our rules, and will then post a status report inside the pull request that is open. + + The PR will be assigned tags depending on the outcome of the code scan. If issues are detected in the code, a list of each file and issue will be posted in the PR. + + - Workflow: `${{ github.workflow }} (#${{github.run_number}})` + - Triggered By: `${{ github.actor }}` + - Status: `${{ job.status == 'success' && '✅ Successful' || '❌ Failed' }}` + + ## ${{ github.event.pull_request.title }} (${{ github.event.pull_request.number }}) + + - Pull Request: ${{ github.event.pull_request.html_url }} + - Author: https://github.com/${{ github.event.pull_request.user.login }} (${{ github.event.pull_request.author_association }}) + - Repo: https://github.com/${{ github.repository }} + - Branch: `${{ github.head_ref }}` + - Author: https://github.com/${{ github.event.pull_request.user.login }} (${{ github.event.pull_request.author_association }}) + - Status: `${{ github.event.pull_request.state }}` + + ### Scan Results + - Added Files: ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }} + - Modified Files: ${{ steps.task_autocheck_changed_files_get.outputs.all_modified_files_count }} + - Renamed Files: ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }} + - Copied Files: ${{ steps.task_autocheck_changed_files_get.outputs.copied_files_count }} + - Deleted Files: ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} + + embed-color: ${{ job.status == 'success' && '5763719' || '15418782' }} + embed-footer-text: "Completed at ${{ env.NOW }} UTC" + embed-timestamp: "${{ env.NOW_LONG }}" + embed-author-name: "${{ github.event.pull_request.user.login }}" + embed-author-url: "${{ github.event.pull_request.html_url }}" + embed-author-icon-url: "${{ github.event.pull_request.user.avatar_url }}" + + # # + # Autoscan › Notify Github › Failure + # # + + - name: >- + 🔔 Send Discord Webhook Message (Failure) + id: task_autocheck_notify_discord_failure + uses: tsickert/discord-webhook@v6.0.0 + if: failure() + with: + username: 'Io' + avatar-url: 'https://i.imgur.com/8BVDkla.jpg' + webhook-url: ${{ secrets.DISCORD_WEBHOOK_CHAN_TVAPP2_WORKFLOWS }} + embed-title: "**Issues › Scan Workflow Ran**" + embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + embed-thumbnail-url: 'https://cdn.pixabay.com/photo/2022/01/30/13/33/github-6980894_960_720.png' + embed-description: | + ## 🎫 Issues › Scan ${{ job.status == 'success' && '✅' || '❌' }} + + **${{ job.status == 'success' && '✅ Success' || '❌ Failure' }}** › Your container just ran the `Issues › Scan` workflow. Every time this workflow is ran, your list of pull requests will be scanned to determine what files have been changed. It will scan each modified file and see if the code conforms with our rules, and will then post a status report inside the pull request that is open. + + The PR will be assigned tags depending on the outcome of the code scan. If issues are detected in the code, a list of each file and issue will be posted in the PR. + + - Workflow: `${{ github.workflow }} (#${{github.run_number}})` + - Triggered By: `${{ github.actor }}` + - Status: `${{ job.status == 'success' && '✅ Successful' || '❌ Failed' }}` + + ## ${{ github.event.pull_request.title }} (${{ github.event.pull_request.number }}) + + - Pull Request: ${{ github.event.pull_request.html_url }} + - Author: https://github.com/${{ github.event.pull_request.user.login }} (${{ github.event.pull_request.author_association }}) + - Repo: https://github.com/${{ github.repository }} + - Branch: `${{ github.head_ref }}` + - Author: https://github.com/${{ github.event.pull_request.user.login }} (${{ github.event.pull_request.author_association }}) + - Status: `${{ github.event.pull_request.state }}` + + ### Scan Results + - Added Files: ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }} + - Modified Files: ${{ steps.task_autocheck_changed_files_get.outputs.all_modified_files_count }} + - Renamed Files: ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }} + - Copied Files: ${{ steps.task_autocheck_changed_files_get.outputs.copied_files_count }} + - Deleted Files: ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} + + embed-color: ${{ job.status == 'success' && '5763719' || '15418782' }} + embed-footer-text: "Completed at ${{ env.NOW }} UTC" + embed-timestamp: "${{ env.NOW_LONG }}" + embed-author-name: "${{ github.event.pull_request.user.login }}" + embed-author-url: "${{ github.event.pull_request.html_url }}" + embed-author-icon-url: "${{ github.event.pull_request.user.avatar_url }}" diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml deleted file mode 100644 index 9d459b9b..00000000 --- a/.github/workflows/issues-stale.yml +++ /dev/null @@ -1,667 +0,0 @@ -# # -# @type github workflow -# @desc creates repository labels if they are not yet installed -# issues marked as stale after 30 days, given tag Status 𐄂 Stale -# inactive issues closed after 180 days, given tag Status 𐄂 Locked -# inactive pr closed after 365 days, given tag Status 𐄂 Locked -# issues marked stale after 30 days, given tag Status 𐄂 Stale -# issues marked closed 7 days after being marked stale, given tag Status 𐄂 Autoclosed -# @author Aetherinox -# @url https://github.com/Aetherinox -# -# This Github action must be activated manually. This workflow script will do the -# following: -# -# - Scan issues / pull requests and make sure they have properly assigned labels: -# - `Bug` -# - `Feature` -# - `Urgent` -# - `Roadmap` -# -# - Workflow script will then scan each pr or issue and mark them as `Stale` -# if they haven't had any replies in 30 days. -# -# - Workflow will `autoclose` pr or issues which haven't had action in `365 days`. -# # - -name: "🎫 Issues › Stale" -run-name: "🎫 Issues › Stale" - -# # -# triggers -# # - -on: - workflow_dispatch: - schedule: - - cron: "0 0 * * *" - -# # -# environment variables -# # - -env: - PREFIX_BUG: "Bug" - PREFIX_DEPENDENCY: "Dependency" - PREFIX_DOCS: "Docs" - PREFIX_FEATURE: "Feature" - PREFIX_GIT: "Git Action" - PREFIX_PR: "PR" - PREFIX_ROADMAP: "Roadmap" - PREFIX_INTERNAL: "Internal" - PREFIX_URGENT: "Urgent" - - LABEL_BUG: "Type ◦ Bug" - LABEL_DEPENDENCY: "Type ◦ Dependency" - LABEL_DOCS: "Type ◦ Docs" - LABEL_FEATURE: "Type ◦ Feature" - LABEL_GIT: "Type ◦ Git Action" - LABEL_PR: "Type ◦ Pull Request" - LABEL_ROADMAP: "Type ◦ Roadmap" - LABEL_INTERNAL: "Type ◦ Internal" - LABEL_URGENT: "⚠ Urgent" - - BOT_NAME_1: EuropaServ - BOT_NAME_DEPENDABOT: dependabot[bot] - LABELS_JSON: | - [ - { "name": "AC › Changes Made", "color": "8F1784", "description": "Requested changes have been made and are pending a re-scan" }, - { "name": "AC › Changes Required", "color": "8F1784", "description": "Requires changes to be made to the package before being accepted" }, - { "name": "AC › Failed", "color": "a61f2d", "description": "Autocheck failed to run through a complete cycle, requires investigation" }, - { "name": "AC › Needs Rebase", "color": "8F1784", "description": "Due to the permissions on the requesting repo, this pull request must be rebased by the author" }, - { "name": "AC › Passed", "color": "146b4a", "description": "Ready to be reviewed" }, - { "name": "AC › Review Required", "color": "8F1784", "description": "PR needs to be reviewed by another person, after the requested changes have been made" }, - { "name": "AC › Security Warning", "color": "761620", "description": "Does not conform to developer policies, or includes potentially dangerous code" }, - { "name": "AC › Skipped Scan", "color": "8F1784", "description": "Author has skipped code scan" }, - { "name": "Status 𐄂 Duplicate", "color": "75536b", "description": "Issue or pull request already exists" }, - { "name": "Status 𐄂 Accepted", "color": "2e7539", "description": "This pull request has been accepted" }, - { "name": "Status 𐄂 Autoclosed", "color": "3E0915", "description": "Originally stale and was autoclosed for no activity" }, - { "name": "Status 𐄂 Denied", "color": "ba4058", "description": "Pull request has been denied" }, - { "name": "Status 𐄂 Locked", "color": "550F45", "description": "Automatically locked by AdminServ for a prolonged period of inactivity" }, - { "name": "Status 𐄂 Need Info", "color": "2E3C4C", "description": "Not enough information to resolve" }, - { "name": "Status 𐄂 No Action", "color": "030406", "description": "Closed without any action being taken" }, - { "name": "Status 𐄂 Pending", "color": "984b12", "description": "Pending pull request" }, - { "name": "Status 𐄂 Released", "color": "1b6626", "description": "Issues or PR has been implemented and is now live" }, - { "name": "Status 𐄂 Reopened", "color": "8a6f14", "description": "A previously closed PR which has been re-opened" }, - { "name": "Status 𐄂 Review", "color": "9e1451", "description": "Currently pending review" }, - { "name": "Status 𐄂 Stale", "color": "928282", "description": "Has not had any activity in over 30 days" }, - { "name": "Type ◦ Bug", "color": "9a2c2c", "description": "Something isn't working" }, - { "name": "Type ◦ Dependency", "color": "243759", "description": "Item is associated to dependency" }, - { "name": "Type ◦ Docs", "color": "0e588d", "description": "Improvements or modifications to docs" }, - { "name": "Type ◦ Feature", "color": "3c4e93", "description": "Feature request" }, - { "name": "Type ◦ Git Action", "color": "030406", "description": "GitHub Action / workflow" }, - { "name": "Type ◦ Pull Request", "color": "8F1784", "description": "Normal pull request" }, - { "name": "Type ◦ Roadmap", "color": "8F1784", "description": "Feature or bug currently planned for implementation" }, - { "name": "Type ◦ Internal", "color": "A51994", "description": "Assigned items are for internal developer use" }, - { "name": "Build ◦ Desktop", "color": "c7ca4a", "description": "Specific to desktop" }, - { "name": "Build ◦ Linux", "color": "c7ca4a", "description": "Specific to Linux" }, - { "name": "Build ◦ MacOS", "color": "c7ca4a", "description": "Specific to MacOS" }, - { "name": "Build ◦ Mobile", "color": "c7ca4a", "description": "Specific to mobile" }, - { "name": "Build ◦ Web", "color": "c7ca4a", "description": "Specific to web" }, - { "name": "Build ◦ Windows", "color": "c7ca4a", "description": "Specific to Windows" }, - { "name": "› API", "color": "F99B50", "description": "Plugin API, CLI, browser JS API" }, - { "name": "› Auto-type", "color": "9141E0", "description": "Auto-type functionality in desktop apps" }, - { "name": "› Browser", "color": "9141E0", "description": "Browser plugins and passing data to <=> from app" }, - { "name": "› Customization", "color": "E3F0FC", "description": "Customizations: plugins, themes, configs" }, - { "name": "› Design", "color": "FA70DE", "description": "Design related queries" }, - { "name": "› Dist", "color": "FA70DE", "description": "Installers and other forms of software distribution" }, - { "name": "› Enterprise", "color": "11447a", "description": "Issues about collaboration, administration, and so on" }, - { "name": "› Hardware", "color": "5a7503", "description": "YubiKey, other tokens, biometrics" }, - { "name": "› Import/Export", "color": "F5FFCC", "description": "Import from and export to different file formats" }, - { "name": "› Improvement", "color": "185c98", "description": "Enhance an existing feature" }, - { "name": "› Performance", "color": "006b75", "description": "Web and desktop performance issues" }, - { "name": "› Plugin Request", "color": "FCE9CA", "description": "Requested changes should be implemented as a plugin" }, - { "name": "› Security", "color": "F75D39", "description": "Security issues" }, - { "name": "› Self-Hosting", "color": "fad8c7", "description": "Self-hosting installations and configs" }, - { "name": "› Storage", "color": "5319e7", "description": "Storage providers: Dropbox, Google, WebDAV, etc." }, - { "name": "› Updater", "color": "1BADDE", "description": "Auto-updater issues" }, - { "name": "› UX", "color": "1BADDE", "description": "UX and usability" }, - { "name": "› Website", "color": "fef2c0", "description": "Website related issues" }, - { "name": "⚠ Urgent", "color": "a8740e", "description": "Requires urgent attention" }, - { "name": "⚠ Announcement", "color": "DB4712", "description": "Announcements" }, - { "name": "📰 Progress Report", "color": "392297", "description": "Development updates" }, - { "name": "📦 Release", "color": "277542", "description": "Release announcements" }, - { "name": "✔️ Poll", "color": "972255", "description": "Community polls" }, - { "name": "❔ Question", "color": "FFFFFF", "description": "All questions" } - ] - -# # -# jobs -# # - -jobs: - - # # - # Job [ Verify / Create Labels ] - # - # This job will ensure you have labels already created in your repo. - # All labels come from the JSON table LABELS_JSON. - # # - - job-labels-create: - name: >- - 🎫 Labels › Verify Existing - runs-on: ubuntu-latest - steps: - - # # - # [ Create Labels ] Start - # # - - - name: >- - ✅ Start - id: task_label_create_start - run: | - echo "Assigning labels and assignees" - - # # - # [ Create Labels ] Checkout - # # - - - name: >- - ☑️ Checkout - id: task_label_create_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # [ Create Labels ] Verify Existing Labels - # # - - - name: >- - 🏷️ Verify Existing Labels - id: task_label_create_verify - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const labels = JSON.parse( process.env.LABELS_JSON ); - for ( const label of labels ) - { - try - { - await github.rest.issues.createLabel( - { - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - description: label.description || '', - color: label.color - }); - } - catch ( err ) - { - if ( err.status === 422 ) - { - console.log( `Label '${label.name}' already exists. Skipping.` ); - } - else - { - console.error( `Error creating label '${label.name}': ${err}` ); - } - } - } - - # # - # Job [ Check Labels ] - # - # Runs through all submissions to check for ones that have not been properly labeled - # - Bug - # - Feature - # - Urgent - # - Roadmap - # # - - job-issues-nolabel: - name: >- - 🎫 Labels › Assign Missing - runs-on: ubuntu-latest - needs: job-labels-create - steps: - - # # - # [ Check Labels ] Checkout - # # - - - name: "☑️ Prepare" - id: task_issues_nolabel_prepare - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # [ Check Labels ] Check - # Check if repo has labels currently added to issues - # # - - - name: 🏷️ Checking Issues - id: task_issues_nolabel_run - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - - /* - Date/Time - */ - - const dateTimeformat = ( date ) => - { - let month = date.getMonth( ) + 1; - month = month.toString( ).padStart( 2, '0' ); - let day = date.getDate( ).toString( ).padStart( 2, '0' ); - let year = date.getFullYear( ).toString( ).padStart( 2, '0' ); - - let hours = date.getHours(); - let minutes = date.getMinutes(); - let x = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; - minutes = minutes.toString( ).padStart( 2, '0' ); - - let mergeTime = month + '.' + day + '.' + year + ' ' + hours + ':' + minutes + ' ' + x; - - return mergeTime; - } - - /* - Change last number ( 36 = hours ) - */ - - const expireAfterMs = 1000 * 60 * 60 * 36; // milliseconds ( 36 hours ) - const curtime = new Date( ).getTime( ); // 1711471510629 - const issues = await github.rest.issues.listForRepo( { owner: context.repo.owner, repo: context.repo.repo, state: 'open' } ); - - console.log( ` 📦── Found ${issues.data.length} open issues` ); - - for ( const issue of issues.data ) - { - - const author = `${ issue.user.login }`; - let date_UpdateDate = new Date( `${ issue.updated_at }` ?? `${ issue.created_at }` ); // Tue Mar 26 2024 16:40:41 GMT+0000 (Coordinated Universal Time) - date_UpdateDate.toISOString( ) // Tue Mar 26 2024 16:40:41 GMT+0000 (Coordinated Universal Time) (string) - - let date_UpdateHuman = dateTimeformat( date_UpdateDate ) + " UTC"; // 03.26.2024 4:40 PM UTC - const time_UpdateMs = new Date( issue.updated_at ).getTime( ); // 1711471241000 - - //if ( curtime < time_UpdateMs + expireAfterMs ) continue; - - /* - Anything past this point is stale / to be closed - */ - - const timeline = await github.rest.issues.listEventsForTimeline( { owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number } ); - // const labelEvent = timeline.data.find( event => event.event === 'labeled' && event.label.name === 'status-stale' ); - - /* - Get Issue Data - */ - - const add_labels = issue.labels.map( label => label.name ); - - let iss_title = `${ issue.title }`; - const iss_title_lc = iss_title.toLowerCase( ); - - let iss_body = `${ issue.body }`; - const iss_body_lc = iss_body.toLowerCase( ); - - console.log( ` └── 📁 ` + iss_title ); - console.log( ` └── 📄 Issue #${ issue.number } last updated on ${ date_UpdateHuman }` ); - console.log( ` └── 📄 ${add_labels}` ); - console.log( `\n\n` ) - - /* - Keywords - */ - - const bug_words = [ "bug", "broke", "issue", "fail" ]; - const feat_words = [ "feature", "request", "add support" ]; - const urgn_words = [ "urgent", "urgency", "emergency", "important", "critical" ]; - const road_words = [ "roadmap", "road map", "planned" ]; - - /* - Tags - */ - - const bug_tag = `${{ env.PREFIX_BUG }}:`; - const bug_lbl = `${{ env.LABEL_BUG }}`; - const feat_tag = `${{ env.PREFIX_FEATURE }}:`; - const feat_lbl = `${{ env.LABEL_FEATURE }}`; - const urgn_tag = `${{ env.PREFIX_URGENT }}:`; - const urgn_lbl = `${{ env.LABEL_URGENT }}`; - const road_tag = `${{ env.PREFIX_ROADMAP }}:`; - const road_lbl = `${{ env.LABEL_ROADMAP }}`; - - /* - Label > Bugs - */ - - const bug_bIncWordT = bug_words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - - Regex: - https://regex101.com/r/Z99Gnq/2 - */ - - const bug_findWordList = /^\b(?:I?\s*have\s*(?:a|an)\s*(?:issue|problem|bug))|(?:will\s*not\s*work)|(?:it\s*is\s*(?:broken|broke|stuck))|(?:found\s*(?:an?|the)\s*(?:bug|issue))|(?:can\s*I\s*fix\s*the\s*(?:bug|issue))|(?:(?:does not|doesn'?t|don'?t|won'?t|can'?t|can\s?not|will\s*not)\s*(?:work|load|function))|(?:it\s*(?:will\s?not|won'?t|can\s?not|can'?t))\s*(?:get|find)\s*the\s*(?:website|site|webpage|page)|(?:the\s*(?:window|frame)\s*is\s*(?:blank|white|empty|missing))\b$/igm; - const bug_bFoundMatchTitle = Boolean( bug_findWordList.test( iss_title ) ); - const bug_bFoundMatchBody = Boolean( bug_findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const bug_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const bug_bFoundPRTitle = Boolean( bug_findPRTitle.test( iss_title ) ); - - /* - - Check if issue title matches the issue label "Bug:" - - Check if title contains word in containsList - */ - - if ( iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) || bug_bIncWordT || bug_bFoundMatchTitle || bug_bFoundMatchBody ) - { - - add_labels.push( `${ bug_lbl }` ); - - if ( author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ author }` ) - - // Rename title to contain Bug: - if ( author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !bug_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - const title = issue.title; - let title_new = title.replace( /^\s?bug\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?fail\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?issue\s*(.*?)\b/gi, '' ); - iss_title = `${ bug_tag } ${ title_new }`; - } - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - /* - Label > Features - */ - - const feat_bIncWordT = feat_words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - - Regex: - https://regex101.com/r/fR1Hm6/1 - */ - - const feat_findWordList = /^(?:(?:request|include|see)\s*(?:an?|the?)\s*(?:feature|addon|addition|plugin))|(?:(?:add|see|get)\s*support\s*(?:for|with|of))|(?:can\s*we\s*get\s*(?:the|a)\s*(?:ability|feature))|(?:💡 Feature:)$/igm; - const feat_bFoundMatchTitle = Boolean( feat_findWordList.test( iss_title ) ); - const feat_bFoundMatchBody = Boolean( feat_findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const feat_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const feat_bFoundPRTitle = Boolean( feat_findPRTitle.test( iss_title ) ); - - /* - - Check if issue title matches the issue label "Feature:" - - Check if title contains word in containsList - */ - - if ( iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) || feat_bIncWordT || feat_bFoundMatchTitle || feat_bFoundMatchBody ) - { - - add_labels.push( `${ feat_lbl }` ); - - if ( author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ author }` ) - - // Rename title to contain Feature: - if ( author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !feat_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - const title = issue.title; - let title_new = title.replace( /^\s?feature\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?request\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?add(.*?)\s?feature\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?add(.*?)\s?support\s*(.*?)\b/gi, '' ); - iss_title = `${ feat_tag } ${ title_new }`; - } - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - /* - Label > Urgent - */ - - const urgn_bIncWordT = urgn_words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - - Regex: - https://regex101.com/r/eE9tJX/2 - */ - - const urgn_findWordList = /(?:(?:this)?is\s*a?n?\s*?(?:emergency|urgent|important|vital|acute|crucial|grave|pressing|serious|top.?priority|high.?priority))|(?:reply|respond|answer|write|address)\s*(?:immediate|quick|asap|urgent|now|fast|(?:as)?\s*(?:soon|quick|immediate|fast))(?:ly)?|(?:need\s*(?:help|support|fixed|answer|reply|response)!)|(?:emergency|critical|urgen(?:t|cy)|high.?priority)/igm; - const urgn_bFoundMatchTitle = Boolean( urgn_findWordList.test( iss_title ) ); - const urgn_bFoundMatchBody = Boolean( urgn_findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const urgn_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const urgn_bFoundPRTitle = Boolean( urgn_findPRTitle.test( iss_title ) ); - - /* - - Check if issue title matches the issue label "Urgent:" - - Check if title contains word in containsList - */ - - if ( iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) || urgn_bIncWordT || urgn_bFoundMatchTitle || urgn_bFoundMatchBody ) - { - - add_labels.push( `${ urgn_lbl }` ); - - if ( author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ author }` ) - - // Rename title to contain Urgent: - if ( author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !urgn_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - const title = issue.title; - let title_new = title.replace( /^\s?emergency\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?urgent\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?urgency\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?important\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?critical\s*(.*?)\b/gi, '' ); - iss_title = `${ urgn_tag } ${ title_new }`; - } - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - /* - Label > Roadmap - */ - - const road_bIncWordT = road_words.some( s => s.includes( iss_title_lc ) || iss_title_lc.includes( s ) ); - - /* - Find regex based phrases - Roadmap requires headers #Summary and #Proposal | #Objective - - Regex: - https://regex101.com/r/ucajBZ/1 - */ - - const road_findWordList = /#\s*Summary[\S\s]+#\s*(?:Proposal|Objective)[^\]]+/igm; - const road_bFoundMatchTitle = Boolean( road_findWordList.test( iss_title ) ); - const road_bFoundMatchBody = Boolean( road_findWordList.test( iss_body ) ); - - /* - Do not change a title if the item starts with a PR: # - - Regex: - https://regex101.com/r/JOrqbN/1 - */ - - const road_findPRTitle = /^PR\s?#?(?:[0-9]*:)/igm; - const road_bFoundPRTitle = Boolean( road_findPRTitle.test( iss_title ) ); - - /* - - Check if issue title matches the issue label "Roadmap:" - - Check if title contains word in containsList - */ - - if ( iss_title_lc.startsWith( road_tag.toLowerCase( ) ) || road_bIncWordT || road_bFoundMatchTitle || road_bFoundMatchBody ) - { - - add_labels.push( `${ road_lbl }` ); - - if ( author === `${{ env.BOT_NAME_DEPENDABOT }}` ) - core.info( `Skipping: Detected ${ author }` ) - - // Rename title to contain Roadmap: - if ( author !== `${{ env.BOT_NAME_DEPENDABOT }}` && !road_bFoundPRTitle && !iss_title_lc.startsWith( bug_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( feat_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( urgn_tag.toLowerCase( ) ) && !iss_title_lc.startsWith( road_tag.toLowerCase( ) ) ) - { - const title = issue.title; - let title_new = title.replace( /^\s?emergency\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?urgent\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?urgency\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?important\s*(.*?)\b/gi, '' ); - title_new = title.replace( /^\s?critical\s*(.*?)\b/gi, '' ); - iss_title = `${ road_tag } ${ title_new }`; - } - - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - title: `${ iss_title }`, labels: add_labels - } ); - } - - /* - await github.rest.issues.update( - { - owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - state: 'closed', state_reason: 'not planned' - } ); - */ - } - - # # - # Job [ Stale Issues ] - # # - - job-issues-stale: - name: >- - 💤 Check › Stale - runs-on: ubuntu-latest - needs: - - job-labels-create - - job-issues-nolabel - permissions: - contents: write - issues: write - pull-requests: write - steps: - - # # - # [ Stale Issues ] Check Condition - # # - - - name: "💤 Stale › Check Condition" - uses: actions/stale@v9 - id: task_issues_stale_run - with: - repo-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - stale-issue-message: | - ⚠️ It looks like there hasn't been any recent updates on this issue. If you created this issue and no longer consider it - open, then please login to github and close the issue. - - If there is no further activity on this issue, it will be automatically closed in the next week. - - --- - - <sub>I am a bot reaching out to you with an automated response.</sub> - stale-issue-label: 'Status 𐄂 Stale' - close-issue-label: 'Status 𐄂 Autoclosed' - exempt-issue-labels: 'Status 𐄂 Accepted,Status 𐄂 Review,Status 𐄂 Pending,Type ◦ Bug,Type ◦ Dependency,Type ◦ Docs,Type ◦ Feature,Type ◦ Git Action,Type ◦ Pull Request,Type ◦ Roadmap' - days-before-stale: 14 - days-before-close: 7 - days-before-pr-stale: -1 - days-before-pr-close: -1 - - # # - # Job [ Lock Issues ] - # # - - job-issues-lock: - name: >- - 🔒 Check › Inactive - runs-on: ubuntu-latest - needs: - - job-labels-create - - job-issues-nolabel - steps: - - # # - # [ Lock Issues ] Look for inactives - # # - - - name: "🔒 Lock › Inactives" - uses: dessant/lock-threads@v5 - id: task_issues_lock_run - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - exclude-any-issue-labels: 'AC › Review Required,Status 𐄂 Accepted,Status 𐄂 Review,Status 𐄂 Pending,Type ◦ Bug,Type ◦ Dependency,Type ◦ Docs,Type ◦ Feature,Type ◦ Git Action,Type ◦ Roadmap,Type ◦ Internal' - add-issue-labels: 'Status 𐄂 Locked' - issue-inactive-days: '60' - issue-lock-reason: 'resolved' - issue-comment: > - ⚠️ This **issue** has been automatically locked since there has not been any recent activity after it was closed. - - Please open a new issue for related bugs. - - --- - - <sub>I am a bot reaching out to you with an automated response.</sub> - exclude-any-pr-labels: 'AC › Review Required,Status 𐄂 Accepted,Status 𐄂 Review,Status 𐄂 Pending,Type ◦ Bug,Type ◦ Dependency,Type ◦ Docs,Type ◦ Feature,Type ◦ Git Action,Type ◦ Roadmap,Type ◦ Internal' - add-pr-labels: 'Status 𐄂 Locked' - pr-inactive-days: '365' - pr-lock-reason: 'resolved' - pr-comment: > - ⚠️ This **pull request** has been automatically locked since there has not been any recent activity after it was closed. - - Please open a new issue for related bugs. - - --- - - <sub>I am a bot reaching out to you with an automated response.</sub> diff --git a/.github/workflows/labels-clean..yml b/.github/workflows/labels-clean..yml deleted file mode 100644 index a7587192..00000000 --- a/.github/workflows/labels-clean..yml +++ /dev/null @@ -1,183 +0,0 @@ -# # -# @type github workflow -# @desc manually activated workflow to remove issue labels -# @author Aetherinox -# @url https://github.com/Aetherinox -# -# This Github action must be activated manually. This workflow script will do the -# following: -# -# - Remove all existing labels in repository -# # - -name: "🎫 Labels › Remove" -run-name: "🎫 Labels › Remove" - -# # -# triggers -# # - -on: - workflow_dispatch: - -# # -# environment variables -# # - -env: - BOT_NAME_1: EuropaServ - BOT_NAME_DEPENDABOT: dependabot[bot] - LABELS_JSON: | - [ - { "name": "AC › Changes Made", "color": "8F1784", "description": "Requested changes have been made and are pending a re-scan" }, - { "name": "AC › Changes Required", "color": "8F1784", "description": "Requires changes to be made to the package before being accepted" }, - { "name": "AC › Failed", "color": "a61f2d", "description": "Autocheck failed to run through a complete cycle, requires investigation" }, - { "name": "AC › Needs Rebase", "color": "8F1784", "description": "Due to the permissions on the requesting repo, this pull request must be rebased by the author" }, - { "name": "AC › Passed", "color": "146b4a", "description": "Ready to be reviewed" }, - { "name": "AC › Review Required", "color": "8F1784", "description": "PR needs to be reviewed by another person, after the requested changes have been made" }, - { "name": "AC › Security Warning", "color": "761620", "description": "Does not conform to developer policies, or includes potentially dangerous code" }, - { "name": "AC › Skipped Scan", "color": "8F1784", "description": "Author has skipped code scan" }, - { "name": "Status 𐄂 Duplicate", "color": "75536b", "description": "Issue or pull request already exists" }, - { "name": "Status 𐄂 Accepted", "color": "2e7539", "description": "This pull request has been accepted" }, - { "name": "Status 𐄂 Autoclosed", "color": "3E0915", "description": "Originally stale and was autoclosed for no activity" }, - { "name": "Status 𐄂 Denied", "color": "ba4058", "description": "Pull request has been denied" }, - { "name": "Status 𐄂 Locked", "color": "550F45", "description": "Automatically locked by AdminServ for a prolonged period of inactivity" }, - { "name": "Status 𐄂 Need Info", "color": "2E3C4C", "description": "Not enough information to resolve" }, - { "name": "Status 𐄂 No Action", "color": "030406", "description": "Closed without any action being taken" }, - { "name": "Status 𐄂 Pending", "color": "984b12", "description": "Pending pull request" }, - { "name": "Status 𐄂 Released", "color": "1b6626", "description": "Issues or PR has been implemented and is now live" }, - { "name": "Status 𐄂 Reopened", "color": "8a6f14", "description": "A previously closed PR which has been re-opened" }, - { "name": "Status 𐄂 Review", "color": "9e1451", "description": "Currently pending review" }, - { "name": "Status 𐄂 Stale", "color": "928282", "description": "Has not had any activity in over 30 days" }, - { "name": "Type ◦ Bug", "color": "9a2c2c", "description": "Something isn't working" }, - { "name": "Type ◦ Dependency", "color": "243759", "description": "Item is associated to dependency" }, - { "name": "Type ◦ Docs", "color": "0e588d", "description": "Improvements or modifications to docs" }, - { "name": "Type ◦ Feature", "color": "3c4e93", "description": "Feature request" }, - { "name": "Type ◦ Git Action", "color": "030406", "description": "GitHub Action / workflow" }, - { "name": "Type ◦ Pull Request", "color": "8F1784", "description": "Normal pull request" }, - { "name": "Type ◦ Roadmap", "color": "8F1784", "description": "Feature or bug currently planned for implementation" }, - { "name": "Type ◦ Internal", "color": "A51994", "description": "Assigned items are for internal developer use" }, - { "name": "Build ◦ Desktop", "color": "c7ca4a", "description": "Specific to desktop" }, - { "name": "Build ◦ Linux", "color": "c7ca4a", "description": "Specific to Linux" }, - { "name": "Build ◦ MacOS", "color": "c7ca4a", "description": "Specific to MacOS" }, - { "name": "Build ◦ Mobile", "color": "c7ca4a", "description": "Specific to mobile" }, - { "name": "Build ◦ Web", "color": "c7ca4a", "description": "Specific to web" }, - { "name": "Build ◦ Windows", "color": "c7ca4a", "description": "Specific to Windows" }, - { "name": "› API", "color": "F99B50", "description": "Plugin API, CLI, browser JS API" }, - { "name": "› Auto-type", "color": "9141E0", "description": "Auto-type functionality in desktop apps" }, - { "name": "› Browser", "color": "9141E0", "description": "Browser plugins and passing data to <=> from app" }, - { "name": "› Customization", "color": "E3F0FC", "description": "Customizations: plugins, themes, configs" }, - { "name": "› Design", "color": "FA70DE", "description": "Design related queries" }, - { "name": "› Dist", "color": "FA70DE", "description": "Installers and other forms of software distribution" }, - { "name": "› Enterprise", "color": "11447a", "description": "Issues about collaboration, administration, and so on" }, - { "name": "› Hardware", "color": "5a7503", "description": "YubiKey, other tokens, biometrics" }, - { "name": "› Import/Export", "color": "F5FFCC", "description": "Import from and export to different file formats" }, - { "name": "› Improvement", "color": "185c98", "description": "Enhance an existing feature" }, - { "name": "› Performance", "color": "006b75", "description": "Web and desktop performance issues" }, - { "name": "› Plugin Request", "color": "FCE9CA", "description": "Requested changes should be implemented as a plugin" }, - { "name": "› Security", "color": "F75D39", "description": "Security issues" }, - { "name": "› Self-Hosting", "color": "fad8c7", "description": "Self-hosting installations and configs" }, - { "name": "› Storage", "color": "5319e7", "description": "Storage providers: Dropbox, Google, WebDAV, etc." }, - { "name": "› Updater", "color": "1BADDE", "description": "Auto-updater issues" }, - { "name": "› UX", "color": "1BADDE", "description": "UX and usability" }, - { "name": "› Website", "color": "fef2c0", "description": "Website related issues" }, - { "name": "⚠ Urgent", "color": "a8740e", "description": "Requires urgent attention" }, - { "name": "⚠ Announcement", "color": "DB4712", "description": "Announcements" }, - { "name": "📰 Progress Report", "color": "392297", "description": "Development updates" }, - { "name": "📦 Release", "color": "277542", "description": "Release announcements" }, - { "name": "✔️ Poll", "color": "972255", "description": "Community polls" }, - { "name": "❔ Question", "color": "FFFFFF", "description": "All questions" } - ] - -# # -# jobs -# # - -jobs: - - # # - # Job › Remove Labels - # - # This job removes all existing labels - # # - - issues-labels-remove: - name: >- - 🎫 Labels › Remove - runs-on: ubuntu-latest - permissions: - contents: 'read' - id-token: 'write' - issues: 'write' - steps: - - # # - # [ Delete Labels ] Start - # # - - - name: >- - ✅ Start - id: task_label_remove_start - run: | - echo "Starting workflow" - - # # - # [ Delete Labels ] Checkout - # # - - - name: >- - ☑️ Checkout - id: task_label_remove_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # [ Delete Labels ] Start - # # - - - name: >- - 🏷️ Delete Existing Labels - id: task_label_remove_run - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const targetOwner = context.repo.owner; - const targetRepo = context.repo.repo; - - // Fetch labels from the source repository - const response = await github.rest.issues.listLabelsForRepo({ - owner: targetOwner, - repo: targetRepo, - }); - console.log("Labels fetched: ", response.data); - - const labels = response.data; - if (labels.length === 0) { - console.log("No labels found in the source repository."); - } - - // Fetch all labels from the target repository and delete them - const existingLabels = await github.rest.issues.listLabelsForRepo({ - owner: targetOwner, - repo: targetRepo, - }); - - // const labels = JSON.parse( process.env.LABELS_JSON ); - for ( const label of labels ) - { - try - { - await github.rest.issues.deleteLabel( - { - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - }); - } - catch ( err ) - { - console.error("Error: " + err); - } - } diff --git a/.github/workflows/labels-create.yml b/.github/workflows/labels-create.yml deleted file mode 100644 index e5e47594..00000000 --- a/.github/workflows/labels-create.yml +++ /dev/null @@ -1,182 +0,0 @@ -# # -# @type github workflow -# @desc manually activated workflow to create issue labels -# @author Aetherinox -# @url https://github.com/Aetherinox -# -# This Github action must be activated manually. This workflow script will do the -# following: -# -# - Scan issues / pull requests and make sure they have properly assigned labels: -# - `Bug` -# - `Feature` -# - `Urgent` -# - `Roadmap` -# -# - Workflow script will then scan each pr or issue and mark them as `Stale` -# if they haven't had any replies in 30 days. -# -# - Workflow will `autoclose` pr or issues which haven't had action in `365 days`. -# # - -name: "🎫 Labels › Create" -run-name: "🎫 Labels › Create" - -# # -# triggers -# # - -on: - workflow_dispatch: - -# # -# environment variables -# # - -env: - BOT_NAME_1: AdminServ - BOT_NAME_2: AdminServX - BOT_NAME_3: EuropaServ - BOT_NAME_DEPENDABOT: dependabot[bot] - LABELS_JSON: | - [ - { "name": "AC › Changes Made", "color": "8F1784", "description": "Requested changes have been made and are pending a re-scan" }, - { "name": "AC › Changes Required", "color": "8F1784", "description": "Requires changes to be made to the package before being accepted" }, - { "name": "AC › Failed", "color": "a61f2d", "description": "Autocheck failed to run through a complete cycle, requires investigation" }, - { "name": "AC › Needs Rebase", "color": "8F1784", "description": "Due to the permissions on the requesting repo, this pull request must be rebased by the author" }, - { "name": "AC › Passed", "color": "146b4a", "description": "Ready to be reviewed" }, - { "name": "AC › Review Required", "color": "8F1784", "description": "PR needs to be reviewed by another person, after the requested changes have been made" }, - { "name": "AC › Security Warning", "color": "761620", "description": "Does not conform to developer policies, or includes potentially dangerous code" }, - { "name": "AC › Skipped Scan", "color": "8F1784", "description": "Author has skipped code scan" }, - { "name": "Status 𐄂 Duplicate", "color": "75536b", "description": "Issue or pull request already exists" }, - { "name": "Status 𐄂 Accepted", "color": "2e7539", "description": "This pull request has been accepted" }, - { "name": "Status 𐄂 Autoclosed", "color": "3E0915", "description": "Originally stale and was autoclosed for no activity" }, - { "name": "Status 𐄂 Denied", "color": "ba4058", "description": "Pull request has been denied" }, - { "name": "Status 𐄂 Locked", "color": "550F45", "description": "Automatically locked by AdminServ for a prolonged period of inactivity" }, - { "name": "Status 𐄂 Need Info", "color": "2E3C4C", "description": "Not enough information to resolve" }, - { "name": "Status 𐄂 No Action", "color": "030406", "description": "Closed without any action being taken" }, - { "name": "Status 𐄂 Pending", "color": "984b12", "description": "Pending pull request" }, - { "name": "Status 𐄂 Released", "color": "1b6626", "description": "Issues or PR has been implemented and is now live" }, - { "name": "Status 𐄂 Reopened", "color": "8a6f14", "description": "A previously closed PR which has been re-opened" }, - { "name": "Status 𐄂 Review", "color": "9e1451", "description": "Currently pending review" }, - { "name": "Status 𐄂 Stale", "color": "928282", "description": "Has not had any activity in over 30 days" }, - { "name": "Type ◦ Bug", "color": "9a2c2c", "description": "Something isn't working" }, - { "name": "Type ◦ Dependency", "color": "243759", "description": "Item is associated to dependency" }, - { "name": "Type ◦ Docs", "color": "0e588d", "description": "Improvements or modifications to docs" }, - { "name": "Type ◦ Feature", "color": "3c4e93", "description": "Feature request" }, - { "name": "Type ◦ Git Action", "color": "030406", "description": "GitHub Action / workflow" }, - { "name": "Type ◦ Pull Request", "color": "8F1784", "description": "Normal pull request" }, - { "name": "Type ◦ Roadmap", "color": "8F1784", "description": "Feature or bug currently planned for implementation" }, - { "name": "Type ◦ Internal", "color": "A51994", "description": "Assigned items are for internal developer use" }, - { "name": "Build ◦ Desktop", "color": "c7ca4a", "description": "Specific to desktop" }, - { "name": "Build ◦ Linux", "color": "c7ca4a", "description": "Specific to Linux" }, - { "name": "Build ◦ MacOS", "color": "c7ca4a", "description": "Specific to MacOS" }, - { "name": "Build ◦ Mobile", "color": "c7ca4a", "description": "Specific to mobile" }, - { "name": "Build ◦ Web", "color": "c7ca4a", "description": "Specific to web" }, - { "name": "Build ◦ Windows", "color": "c7ca4a", "description": "Specific to Windows" }, - { "name": "› API", "color": "F99B50", "description": "Plugin API, CLI, browser JS API" }, - { "name": "› Auto-type", "color": "9141E0", "description": "Auto-type functionality in desktop apps" }, - { "name": "› Browser", "color": "9141E0", "description": "Browser plugins and passing data to <=> from app" }, - { "name": "› Customization", "color": "E3F0FC", "description": "Customizations: plugins, themes, configs" }, - { "name": "› Design", "color": "FA70DE", "description": "Design related queries" }, - { "name": "› Dist", "color": "FA70DE", "description": "Installers and other forms of software distribution" }, - { "name": "› Enterprise", "color": "11447a", "description": "Issues about collaboration, administration, and so on" }, - { "name": "› Hardware", "color": "5a7503", "description": "YubiKey, other tokens, biometrics" }, - { "name": "› Import/Export", "color": "F5FFCC", "description": "Import from and export to different file formats" }, - { "name": "› Improvement", "color": "185c98", "description": "Enhance an existing feature" }, - { "name": "› Performance", "color": "006b75", "description": "Web and desktop performance issues" }, - { "name": "› Plugin Request", "color": "FCE9CA", "description": "Requested changes should be implemented as a plugin" }, - { "name": "› Security", "color": "F75D39", "description": "Security issues" }, - { "name": "› Self-Hosting", "color": "fad8c7", "description": "Self-hosting installations and configs" }, - { "name": "› Storage", "color": "5319e7", "description": "Storage providers: Dropbox, Google, WebDAV, etc." }, - { "name": "› Updater", "color": "1BADDE", "description": "Auto-updater issues" }, - { "name": "› UX", "color": "1BADDE", "description": "UX and usability" }, - { "name": "› Website", "color": "fef2c0", "description": "Website related issues" }, - { "name": "⚠ Urgent", "color": "a8740e", "description": "Requires urgent attention" }, - { "name": "⚠ Announcement", "color": "DB4712", "description": "Announcements" }, - { "name": "📰 Progress Report", "color": "392297", "description": "Development updates" }, - { "name": "📦 Release", "color": "277542", "description": "Release announcements" }, - { "name": "✔️ Poll", "color": "972255", "description": "Community polls" }, - { "name": "❔ Question", "color": "FFFFFF", "description": "All questions" } - ] - -# # -# jobs -# # - -jobs: - - # # - # Job [ Verify / Create Labels ] - # - # This job will ensure you have labels already created in your repo. - # All labels come from the JSON table LABELS_JSON. - # # - - issues-labels-create: - name: >- - 🎫 Labels › Create - runs-on: ubuntu-latest - permissions: - contents: 'read' - id-token: 'write' - issues: 'write' - steps: - - # # - # [ Create Labels ] Start - # # - - - name: >- - ✅ Start - id: task_label_create_start - run: | - echo "Assigning labels and assignees" - - # # - # [ Create Labels ] Checkout - # # - - - name: >- - ☑️ Checkout - id: task_label_create_checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # # - # [ Create Labels ] Verify Existing Labels - # # - - - name: >- - 🏷️ Verify Existing Labels - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.ADMINSERV_TOKEN_CL }} - script: | - const labels = JSON.parse( process.env.LABELS_JSON ); - for ( const label of labels ) - { - try - { - await github.rest.issues.createLabel( - { - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - description: label.description || '', - color: label.color - }); - } - catch ( err ) - { - if ( err.status === 422 ) - { - console.log( `Label '${label.name}' already exists. Skipping.` ); - } - else - { - console.error( `Error creating label '${label.name}': ${err}` ); - } - } - } diff --git a/.github/workflows/ping-developer.yml b/.github/workflows/ping-developer.yml deleted file mode 100644 index d5f5cd42..00000000 --- a/.github/workflows/ping-developer.yml +++ /dev/null @@ -1,175 +0,0 @@ -# # -# @type github workflow -# @desc pings the developer -# @author Aetherinox -# @url https://github.com/Aetherinox -# # - -name: "⚙️ Ping › Developer" -run-name: "⚙️ Ping › Developer" - -# # -# triggers -# # - -on: - issue_comment: - types: [created] - -# # -# environment variables -# # - -env: - BOT_NAME_1: AdminServ - BOT_NAME_2: AdminServX - BOT_NAME_3: EuropaServ - BOT_NAME_DEPENDABOT: dependabot[bot] - -# # -# jobs -# -# env not available for job.if -# # - -jobs: - deploy: - runs-on: ubuntu-latest - if: | - contains(github.event.comment.body, '/ping') - steps: - - # # - # Job > Complete > Get publish timestamp - # # - - - name: "🕛 Get Timestamp" - id: task_complete_timestamp_get - run: | - echo "NOW=$(date +'%m-%d-%Y %H:%M:%S')" >> $GITHUB_ENV - - # # - # Add Label to accepted PR - # - # port 465 - # server_port: 465 - # secure: true - # ignore_cert: false - # - # port 587 - # server_port: 587 - # secure: false - # # - - - name: Send mail - uses: dawidd6/action-send-mail@v4 - with: - server_address: ${{secrets.EMAIL_SMTP}} - server_port: 465 - secure: true - username: ${{secrets.EMAIL_FROM}} - password: ${{secrets.EMAIL_KEY}} - subject: "Github: Ping notification from ${{ github.repository }}" - to: ${{secrets.EMAIL_TO}} - from: ${{secrets.EMAIL_FROM}} - html_body: | - <!DOCTYPE html> - <html> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - <title>Title - - - - - -
-
-
- -
-
- -
-

[Github] Dear ${{github.repository_owner}},

-


You have received a ping notification from ${{ github.repository }} by ${{ github.event.comment.user.login }}.

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
Repository${{ github.repository }}
Date${{ env.NOW }}
Commenter${{ github.event.comment.user.login }}
Issue #${{ github.event.issue.number }}
ActionNotification
-
- -

- -
-
- - - -
-
- -

 

-


~ Github -

-
- -

- -
- Copyright © 2024 - Betelgeuse -
-
- - - ignore_cert: true - convert_markdown: true - priority: normal diff --git a/.gitignore b/.gitignore index 77ea0e07..d323884e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,64 +1,73 @@ # # -# Windows image file caches +# Binaries # # -Thumbs.db -ehthumbs.db +*.exe +*.exe~ +*.dll +*.so +*.dylib # # -# Folder config file +# TVApp2 Specific # # -Desktop.ini +*.dat +*.xml +*.txt # # -# Recycle Bin used on file shares +# Test binary +# build with `go test -c` # # -$RECYCLE.BIN/ +*.test # # -# Windows Installer files +# Output for go coverage tool, specifically when used with LiteIDE # # -*.cab -*.msi -*.msm -*.msp +*.out # # -# Windows shortcuts +# Dependency directories (remove the comment below to include it) +# vendor/ # # -*.lnk - # # -# Operating System Files +# Go workspace file # # -.DS_Store -.AppleDouble -.LSOverride +go.work # # -# Thumbnails +# Mac # # -._* +.DS_STORE # # -# Other +# Visual Studio Code # # -.Spotlight-V100 -.Trashes +.vscode/ # # -# Directories potentially created on remote AFP share +# Temp folders # # -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk \ No newline at end of file +.temp/ +temp/ +work/ + +# # +# Python Cache +# # + +__pycache__/ + +# # +# logs +# # + +.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c07246f7..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,389 +0,0 @@ -
-
Thank you for your interest in contributing!
-

♾️ Contributing ♾️

- -
- - -[![Version][github-version-img]][github-version-uri] -[![Build Status][github-build-img]][github-build-uri] -[![Downloads][github-downloads-img]][github-downloads-uri] -[![Size][github-size-img]][github-size-img] -[![Last Commit][github-commit-img]][github-commit-img] -[![Contributors][contribs-all-img]](#contributors-) - - -
- -
- ---- - -
- -## About - -Below are a list of ways that you can help contribute to this project, as well as policies and guides that explain how to get started. - -Please review everything on this page before you submit your contribution. - -
- ---- - -
- -- [About](#about) -- [Issues, Bugs, Ideas](#issues-bugs-ideas) -- [Contributing](#contributing) - - [Before Submitting Pull Requests](#before-submitting-pull-requests) - - [Conventional Commit Specification](#conventional-commit-specification) - - [Types](#types) - - [Example 1:](#example-1) - - [Example 2:](#example-2) - - [Commiting](#commiting) - - [Commenting](#commenting) - - [Casing](#casing) - - [Indentation Style](#indentation-style) - - [Spaces Instead Of Tabs](#spaces-instead-of-tabs) - -
- ---- - -
- -## Issues, Bugs, Ideas -Stuff happens, and sometimes as best as we try, there may be issues within this project that we are unaware of. That is the great thing about open-source; anyone can use the program and contribute to making it better. - -
- -If you have found a bug, have an issue, or maybe even a cool idea; you can let us know by [submitting it](https://github.com/aetherinox/thetvapp-docker/issues). However, before you submit your new issue, bug report, or feature request; head over to the [Issues Section](https://github.com/aetherinox/thetvapp-docker/issues) and ensure nobody else has already submitted it. - -
- -Once you are sure that your issue has not already being dealt with; you may submit a new issue at [here](https://github.com/aetherinox/thetvapp-docker/issues/new/choose). You'll be asked to specify exactly what your new submission targets, such as: -- Bug report -- Feature Suggestion - -
- -When writing a new submission; ensure you fill out any of the questions asked of you. If you do not provide enough information, we cannot help. Be as detailed as possible, and provide any logs or screenshots you may have to help us better understand what you mean. Failure to fill out the submission properly may result in it being closed without a response. - -
- -If you are submitting a bug report: - -- Explain the issue -- Describe how you expect for a feature to work, and what you're seeing instead of what you expected. -- List possible options for a resolution or insight -- Provide screenshots, logs, or anything else that can visually help track down the issue. - -
- -
- -[![Submit Issue][btn-github-submit-img]][btn-github-submit-uri] - -
- -
- -
- -**[`^ back to top ^`](#about)** - -
- -
- ---- - -
- -## Contributing -If you are looking to contribute to this project by actually submit your own code; please review this section completely. There is important information and policies provided below that you must follow for your pull request to get accepted. - -The source is here for everyone to collectively share and colaborate on. If you think you have a possible solution to a problem; don't be afraid to get your hands dirty. - -All contributions are made via pull requests. To create a pull request, you need a GitHub account. If you are unclear on this process, see [GitHub's documentation on forking and pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). Pull requests should be targeted at the master branch. - -
- -### Before Submitting Pull Requests - -- Follow the repository's code formatting conventions (see below); -- Include tests that prove that the change works as intended and does not add regressions; -- Document the changes in the code and/or the project's documentation; -- Your PR must pass the CI pipeline; -- When submitting your Pull Request, use one of the following branches: - - For bug fixes: `main` branch - - For features & functionality: `development` branch -- Include a proper git commit message following the [Conventional Commit Specification](https://www.conventionalcommits.org/en/v1.0.0/#specification). - -
- -If you have completed the above tasks, the pull request is ready to be reviewed and your pull request's label will be changed to "Ready for Review". At this point, a human will need to step in and manually verify your submission. - -Reviewers will approve the pull request once they are satisfied with the patch it will be merged. - -
- -### Conventional Commit Specification - -When commiting your changes, we require you to follow the [Conventional Commit Specification](https://www.conventionalcommits.org/en/v1.0.0/#specification). The **Conventional Commits** is a specification for the format and content of a commit message. The concept behind Conventional Commits is to provide a rich commit history that can be read and understood by both humans and automated tools. Conventional Commits have the following format: - -
- -``` -[(optional )]: - -[optional ] - -[optional ] -``` - -
- -#### Types -| Type | Description | -| --- | --- | -| `feat` | Introduce new feature | -| `fix` | Bug fix | -| `deps` | Add or update existing dependencies | -| `docs` | Change website or markdown documents. Does not mean changes to the documentation generator script itself, only the documents created from the generator.

E.g: documentation, readme.md or markdown

| -| `build` | Changes to the build / compilation / packaging process or auxiliary tools such as doc generation

E.g: create new build tasks, update release script, etc. | -| `test` | Add or refactor tests, no production code change. Changes the suite of automated tests for the app. | -| `perf` | Performance improvement of algorithms or execution time of the app. Does not change an existing feature. | -| `style` | Update / reformat style of source code. Does not change the way app is implemented. Changes that do not affect the meaning of the code

E.g: white-space, formatting, missing semi-colons, change tabs to spaces, etc) | -| `refactor` | Change to production code that leads to no behavior difference,

E.g: split files, rename variables, rename package, improve code style, etc. | -| `change` | Change an existing feature. | -| `chore` | Includes technical or preventative maintenance task that is necessary for managing the app or repo, such as updating grunt tasks, but is not tied to any specific feature. Usually done for maintanence purposes.

E.g: Edit .gitignore, .prettierrc, .prettierignore, .gitignore, eslint.config.js file | -| `ci` | Changes related to Continuous Integration (usually `yml` and other configuration files). | -| `misc` | Anything that doesn't fit into another commit type. Usually doesn't change production code; yet is not ci, test or chore. | -| `revert` | Revert a previous commit | -| `remove` | Remove a feature from app. Features are usually first deprecated for a period of time before being removed. Removing a feature from the app may be considered a breaking change that will require a major version number increment.| -| `deprecate` | Deprecate existing functionality, but does not remove it from the app.| - -
- -##### Example 1: - -``` -feat(core): bug affecting menu [#22] -^───^────^ ^────────────────^ ^───^ -| | | | -| | | └───⫸ (ISSUE): Reference issue ID -│ │ │ -│ │ └───⫸ (DESC): Summary in present tense. Use lower case not title case! -│ │ -│ └───────────⫸ (SCOPE): The package(s) that this change affects -│ -└───────────────⫸ (TYPE): See list above -``` - -
- -##### Example 2: -``` -(): [issue] - | | | | - | | | └─⫸ Reference issue id (optional) - │ │ │ - │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end. - │ │ - │ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core| - │ elements|forms|http|language-service|localize|platform-browser| - │ platform-browser-dynamic|platform-server|router|service-worker| - │ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve| - │ devtools.... - │ - └─⫸ Commit Type: build|ci|doc|docs|feat|fix|perf|refactor|test - website|chore|style|type|revert|deprecate -``` - -
- -### Commiting -If you are pushing a commit which addresses a submitted issue, reference your issue at the end of the commit message. You may also optionally add the major issue to the end of your commit body. - -References should be on their own line, following the word `Ref` or `Refs` - -``` -Title: fix(core): fix error message displayed to users. [#22] -Description: The description of your commit - - Ref: #22, #34, #37 -``` - -
- -### Commenting - -Comment your code. If someone else comes along, they should be able to do a quick glance and have an idea of what is going on. Plus it helps novice readers to better understand the process. - -You may use block style commenting, or single lines: - -```bash -# # -# set perms and import user crontabs -# # - -checkown "${cron_user}":"${cron_user}" "/config/crontabs/${cron_user}" -crontab -u "${cron_user}" "/config/crontabs/${cron_user}" -``` - -
- -At the top of any new file introduced, please add the following header: - -```bash -#!/usr/bin/with-contenv bash -# shellcheck shell=bash - -# # -# @project thetvapp-docker -# @about DESCRIPTION OF WHAT FILE DOES -# @file /path/to/file.ext -# @repo https://github.com/Aetherinox/thetvapp-docker -# # -``` - -
- -### Casing - -When calling environment variables, you should use `UPPERCASE`: - -```bash -arg_cron=$(echo ${CRON_TIME}) -if [ -z "${arg_cron}" ]; then - arg_cron="0/60 * * * *" -fi -``` - -
- -When defining general variables, use `snake_case` - -```bash -migrations_dir="/migrations" -migrations_history="/config/.migrations" -``` - -
- -### Indentation Style -You should be using the `Allman Style`. This style puts the brace associated with a control statement on the next line, indented. Statements within the braces are indented to the same level as the braces. - -
- -```javascript -location ~ ^(.+\.php)(.*)$ -{ - # enable the next two lines for http auth - # auth_basic "Restricted"; - # auth_basic_user_file /config/nginx/.htpasswd; - - fastcgi_split_path_info ^(.+\.php)(.*)$; - if (!-f $document_root$fastcgi_script_name) { return 404; } - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - include /etc/nginx/fastcgi_params; -} - -# deny access to .htaccess/.htpasswd files -location ~ /\.ht -{ - deny all; -} -``` - -
- -### Spaces Instead Of Tabs -When writing your code, set your IDE to utilize **spaces**, with a configured size of `4 characters`. If this project utilizes ESLint, you should find the file `.editorconfig` in the root directory of the repo which defines how the file should be formatted. Load that file into programs such as Visual Studio Code. - -
- -
- -
- -**[`^ back to top ^`](#about)** - -
- -
-
- - - - - - [general-npmjs-uri]: https://npmjs.com - [general-nodejs-uri]: https://nodejs.org - [general-npmtrends-uri]: http://npmtrends.com/thetvapp-docker - - - [github-version-img]: https://img.shields.io/github/v/tag/Aetherinox/thetvapp-docker?logo=GitHub&label=Version&color=ba5225 - [github-version-uri]: https://github.com/Aetherinox/thetvapp-docker/releases - - - [npm-version-img]: https://img.shields.io/npm/v/thetvapp-docker?logo=npm&label=Version&color=ba5225 - [npm-version-uri]: https://npmjs.com/package/thetvapp-docker - - - [pypi-version-img]: https://img.shields.io/pypi/v/thetvapp-docker-plugin - [pypi-version-uri]: https://pypi.org/project/thetvapp-docker-plugin/ - - - [license-mit-img]: https://img.shields.io/badge/MIT-FFF?logo=creativecommons&logoColor=FFFFFF&label=License&color=9d29a0 - [license-mit-uri]: https://github.com/Aetherinox/thetvapp-docker/blob/main/LICENSE - - - [github-downloads-img]: https://img.shields.io/github/downloads/Aetherinox/thetvapp-docker/total?logo=github&logoColor=FFFFFF&label=Downloads&color=376892 - [github-downloads-uri]: https://github.com/Aetherinox/thetvapp-docker/releases - - - [npmjs-downloads-img]: https://img.shields.io/npm/dw/%40aetherinox%2Fmkdocs-link-embeds?logo=npm&&label=Downloads&color=376892 - [npmjs-downloads-uri]: https://npmjs.com/package/thetvapp-docker - - - [github-size-img]: https://img.shields.io/github/repo-size/Aetherinox/thetvapp-docker?logo=github&label=Size&color=59702a - [github-size-uri]: https://github.com/Aetherinox/thetvapp-docker/releases - - - [npmjs-size-img]: https://img.shields.io/npm/unpacked-size/thetvapp-docker/latest?logo=npm&label=Size&color=59702a - [npmjs-size-uri]: https://npmjs.com/package/thetvapp-docker - - - [codecov-coverage-img]: https://img.shields.io/codecov/c/github/Aetherinox/thetvapp-docker?token=MPAVASGIOG&logo=codecov&logoColor=FFFFFF&label=Coverage&color=354b9e - [codecov-coverage-uri]: https://codecov.io/github/Aetherinox/thetvapp-docker - - - [contribs-all-img]: https://img.shields.io/github/all-contributors/Aetherinox/thetvapp-docker?logo=contributorcovenant&color=de1f6f&label=contributors - [contribs-all-uri]: https://github.com/all-contributors/all-contributors - - - [github-build-img]: https://img.shields.io/github/actions/workflow/status/Aetherinox/thetvapp-docker/deploy-docker.yml?logo=github&logoColor=FFFFFF&label=Build&color=%23278b30 - [github-build-uri]: https://github.com/Aetherinox/thetvapp-docker/actions/workflows/deploy-docker.yml - - - [github-build-pypi-img]: https://img.shields.io/github/actions/workflow/status/Aetherinox/thetvapp-docker/release-pypi.yml?logo=github&logoColor=FFFFFF&label=Build&color=%23278b30 - [github-build-pypi-uri]: https://github.com/Aetherinox/thetvapp-docker/actions/workflows/pypi-release.yml - - - [github-tests-img]: https://img.shields.io/github/actions/workflow/status/Aetherinox/thetvapp-docker/npm-tests.yml?logo=github&label=Tests&color=2c6488 - [github-tests-uri]: https://github.com/Aetherinox/thetvapp-docker/actions/workflows/npm-tests.yml - - - [github-commit-img]: https://img.shields.io/github/last-commit/Aetherinox/thetvapp-docker?logo=conventionalcommits&logoColor=FFFFFF&label=Last%20Commit&color=313131 - [github-commit-uri]: https://github.com/Aetherinox/thetvapp-docker/commits/main/ - - - [btn-github-submit-img]: https://img.shields.io/badge/submit%20new%20issue-de1f5c?style=for-the-badge&logo=github&logoColor=FFFFFF - [btn-github-submit-uri]: https://github.com/aetherinox/thetvapp-docker/issues - - - diff --git a/Dockerfile b/Dockerfile index 50d4a40a..c0be91e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,118 +1,127 @@ # syntax=docker/dockerfile:1 # # +# @project TVApp2 +# @usage docker image which allows you to download a m3u playlist and EPG guide data from +# multiple IPTV services. # @file Dockerfile -# @about This docker file installs: -# - nginx -# - php-fpm -# - theapptv +# @repo https://github.com/iFlip721/tvapp2 +# https://github.com/aetherinox/tvapp2 +# https://github.com/aetherinox/docker-base-alpine +# https://git.binaryninja.net/pub_projects/tvapp2 +# +# you can build your own image by running +# amd64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 -t tvapp2:1.0.0-amd64 -f Dockerfile . +# arm64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:1.0.0-arm64 -f Dockerfile.aarch64 . +# +# if you prefer to use `docker buildx` +# create docker buildx create --driver docker-container --name container --bootstrap --use +# amd64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/amd64 --output type=docker --output type=docker . +# arm64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/arm64 --output type=docker --output type=docker . # # -# # -# Base Image -# This container uses a modified version of the Linux server alpine image -# # -FROM ghcr.io/linuxserver/baseimage-alpine:3.20 - -# # -# Set Labels -# # - -LABEL maintainer="Aetherinox" -LABEL org.opencontainers.image.authors="Aetherinox" -LABEL org.opencontainers.image.vendor="Aetherinox" -LABEL org.opencontainers.image.title="TheTVApp Grabber" -LABEL org.opencontainers.image.description="thetvapp image by Aetherinox" -LABEL org.opencontainers.image.source="https://github.com/Aetherinox/thetvapp-docker" -LABEL org.opencontainers.image.documentation="https://github.com/Aetherinox/thetvapp-docker" -LABEL org.opencontainers.image.url="https://github.com/Aetherinox/thetvapp-docker" -LABEL org.opencontainers.image.licenses="MIT" -LABEL build_version="1.0.0" +FROM ghcr.io/aetherinox/alpine-base:3.20-amd64 # # # Set Args # # -ARG BUILD_DATE +ARG BUILDDATE ARG VERSION -ARG NGINX_VERSION -ARG CRON_TIME -ENV CRON_TIME="0/60 * * * *" + +# # +# Set Labels +# # + +LABEL maintainer="aetherinox, iFlip721" +LABEL org.opencontainers.image.authors="aetherinox, iFlip721" +LABEL org.opencontainers.image.vendor="BinaryNinja" +LABEL org.opencontainers.image.title="TvApp m3u playlist and EPG guide downloader" +LABEL org.opencontainers.image.description="Download m3u playlist and EPG guide data for the IPTV service TheTVApp" +LABEL org.opencontainers.image.source="https://git.binaryninja.net/pub_projects/tvapp2" +LABEL org.opencontainers.image.documentation="https://git.binaryninja.net/pub_projects/tvapp2/wiki" +LABEL org.opencontainers.image.url="https://git.binaryninja.net/pub_projects/tvapp2/packages" +LABEL org.opencontainers.image.licenses="MIT" +LABEL build_version="TvApp2 v${VERSION} build-date: ${BUILDDATE}" + +# # +# Set Env Var +# # + ENV TZ="Etc/UTC" - -ENV URL_XML="https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml" -ENV URL_XML_GZ="https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml.gz" -ENV URL_M3U="https://thetvapp-m3u.data-search.workers.dev/playlist" -ENV FILE_NAME="thetvapp" - -ENV PORT_HTTP=80 -ENV PORT_HTTPS=443 +ENV URL_REPO_BASE="https://github.com/aetherinox/alpine-base/pkgs/container/alpine-base" +ENV URL_REPO_APP="https://git.binaryninja.net/pub_projects/tvapp2" +ENV FILE_NAME="index.html" +ENV PORT_HTTP=4124 +ENV NODE_VERSION=18.20.5 +ENV YARN_VERSION=1.22.22 # # # Install # # RUN \ - if [ -z ${NGINX_VERSION+x} ]; then \ - NGINX_VERSION=$(curl -sL "http://dl-cdn.alpinelinux.org/alpine/v3.20/main/x86_64/APKINDEX.tar.gz" | tar -xz -C /tmp \ - && awk '/^P:nginx$/,/V:/' /tmp/APKINDEX | sed -n 2p | sed 's/^V://'); \ - fi && \ - apk add --no-cache \ - wget \ - logrotate \ - openssl \ - apache2-utils \ - nginx \ - nginx==${NGINX_VERSION} \ - nginx-mod-http-fancyindex==${NGINX_VERSION} && \ - echo "**** Install Build Packages ****" && \ - echo "**** Configure Nginx ****" && \ - echo 'fastcgi_param HTTP_PROXY ""; # https://httpoxy.org/' >> \ - /etc/nginx/fastcgi_params && \ - echo 'fastcgi_param PATH_INFO $fastcgi_path_info; # http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_split_path_info' >> \ - /etc/nginx/fastcgi_params && \ - echo 'fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/#connecting-nginx-to-php-fpm' >> \ - /etc/nginx/fastcgi_params && \ - echo 'fastcgi_param SERVER_NAME $host; # Send HTTP_HOST as SERVER_NAME. If HTTP_HOST is blank, send the value of server_name from nginx (default is `_`)' >> \ - /etc/nginx/fastcgi_params && \ - rm -f /etc/nginx/http.d/default.conf && \ - rm -f /etc/nginx/conf.d/stream.conf && \ - rm -f /config/www/index.html && \ - echo "**** Setup Logrotate ****" && \ - sed -i "s#/var/log/messages {}.*# #g" \ - /etc/logrotate.conf && \ - sed -i 's#/usr/sbin/logrotate /etc/logrotate.conf#/usr/sbin/logrotate /etc/logrotate.conf -s /config/log/logrotate.status#g' \ - /etc/periodic/daily/logrotate + apk add --no-cache \ + wget \ + bash \ + nano \ + npm \ + openssl + +# # +# Copy docker-entrypoint +# # + +COPY docker-entrypoint.sh /usr/local/bin/ # # # Set work directory # # -WORKDIR /config/www +WORKDIR /usr/src/app # # -# add local files +# copy node package.json to workdir +# # + +COPY package*.json ./ + +# # +# install node (production) +# # + +RUN npm install --only=production + +# # +# Add local files +# # + +COPY . . +# COPY node_modules/ package.json package-lock.json formatted.dat index.js ./ + +# # +# when copying with the command above, all files in root folder will be copied. +# # + +RUN rm -rf ./root +RUN rm ./Dockerfile ./Dockerfile.aarch64 docker-entrypoint.sh + +# # +# copy s6-overlays root to image root # # COPY root/ / # # -# ports and volumes +# Ports and volumes # # -EXPOSE ${PORT_HTTP} ${PORT_HTTPS} - -# # -# Add Cron Task Files -# # - -ADD run.sh / -ADD download.sh / +EXPOSE ${PORT_HTTP}/tcp # # # In case user sets up the cron for a longer duration, do a first run # and then keep the container running. Hacky, but whatever. # # -CMD ["sh", "-c", "/run.sh ; /download.sh ; tail -f /dev/null"] +CMD ["sh", "-c", "npm start"] diff --git a/Dockerfile-php.template b/Dockerfile-php.template deleted file mode 100644 index 200aa148..00000000 --- a/Dockerfile-php.template +++ /dev/null @@ -1,134 +0,0 @@ -# syntax=docker/dockerfile:1 - -# # -# @file Dockerfile.IncPhp -# @about This docker file installs: -# - nginx -# - php-fpm -# - theapptv -# # - -# # -# Base Image -# This container uses a modified version of the Linux server alpine image -# # - -FROM ghcr.io/linuxserver/baseimage-alpine:3.20 - -# # -# Set Labels -# # - -LABEL maintainer="Aetherinox" -LABEL org.opencontainers.image.authors="Aetherinox" -LABEL org.opencontainers.image.vendor="Aetherinox" -LABEL org.opencontainers.image.title="TheTVApp Grabber" -LABEL org.opencontainers.image.description="thetvapp image by Aetherinox" -LABEL org.opencontainers.image.source="https://github.com/Aetherinox/thetvapp-docker" -LABEL org.opencontainers.image.documentation="https://github.com/Aetherinox/thetvapp-docker" -LABEL org.opencontainers.image.url="https://github.com/Aetherinox/thetvapp-docker" -LABEL org.opencontainers.image.licenses="MIT" -LABEL build_version="1.0.0" - -# # -# Set Args -# # - -ARG BUILD_DATE -ARG VERSION -ARG NGINX_VERSION -ARG CRON_TIME -ENV CRON_TIME="0/60 * * * *" -ENV TZ="Etc/UTC" - -ENV URL_XML="https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml" -ENV URL_XML_GZ="https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml.gz" -ENV URL_M3U="https://thetvapp-m3u.data-search.workers.dev/playlist" -ENV FILE_NAME="thetvapp" - -ENV PORT_HTTP=80 -ENV PORT_HTTPS=443 - -# # -# Install -# # - -RUN \ - if [ -z ${NGINX_VERSION+x} ]; then \ - NGINX_VERSION=$(curl -sL "http://dl-cdn.alpinelinux.org/alpine/v3.20/main/x86_64/APKINDEX.tar.gz" | tar -xz -C /tmp \ - && awk '/^P:nginx$/,/V:/' /tmp/APKINDEX | sed -n 2p | sed 's/^V://'); \ - fi && \ - apk add --no-cache \ - wget \ - logrotate \ - openssl \ - apache2-utils \ - nginx \ - php83 \ - php83-fileinfo \ - php83-fpm \ - php83-mbstring \ - nginx==${NGINX_VERSION} \ - nginx-mod-http-fancyindex==${NGINX_VERSION} && \ - echo "**** Install Build Packages ****" && \ - echo "**** Configure Nginx ****" && \ - echo 'fastcgi_param HTTP_PROXY ""; # https://httpoxy.org/' >> \ - /etc/nginx/fastcgi_params && \ - echo 'fastcgi_param PATH_INFO $fastcgi_path_info; # http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_split_path_info' >> \ - /etc/nginx/fastcgi_params && \ - echo 'fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/#connecting-nginx-to-php-fpm' >> \ - /etc/nginx/fastcgi_params && \ - echo 'fastcgi_param SERVER_NAME $host; # Send HTTP_HOST as SERVER_NAME. If HTTP_HOST is blank, send the value of server_name from nginx (default is `_`)' >> \ - /etc/nginx/fastcgi_params && \ - rm -f /etc/nginx/http.d/default.conf && \ - rm -f /etc/nginx/conf.d/stream.conf && \ - rm -f /config/www/index.html && \ - echo "**** Check PHP version and symlink ****" && \ - if [ "$(readlink /usr/bin/php)" != "php83" ]; then \ - rm -rf /usr/bin/php && \ - ln -s /usr/bin/php83 /usr/bin/php; \ - fi && \ - echo "**** Configure PHP ****" && \ - sed -i "s#;error_log = log/php83/error.log.*#error_log = /config/log/php/error.log#g" \ - /etc/php83/php-fpm.conf && \ - sed -i "s#user = nobody.*#user = abc#g" \ - /etc/php83/php-fpm.d/www.conf && \ - sed -i "s#group = nobody.*#group = abc#g" \ - /etc/php83/php-fpm.d/www.conf && \ - echo "**** Setup Logrotate ****" && \ - sed -i "s#/var/log/messages {}.*# #g" \ - /etc/logrotate.conf && \ - sed -i 's#/usr/sbin/logrotate /etc/logrotate.conf#/usr/sbin/logrotate /etc/logrotate.conf -s /config/log/logrotate.status#g' \ - /etc/periodic/daily/logrotate - -# # -# Set work directory -# # - -WORKDIR /config/www - -# # -# add local files -# # - -COPY root/ / - -# # -# ports and volumes -# # - -EXPOSE ${PORT_HTTP} ${PORT_HTTPS} - -# # -# Add Cron Task Files -# # - -ADD run.sh / -ADD download.sh / - -# # -# In case user sets up the cron for a longer duration, do a first run -# and then keep the container running. Hacky, but whatever. -# # - -CMD ["sh", "-c", "/run.sh ; /download.sh ; tail -f /dev/null"] diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 new file mode 100644 index 00000000..d66015cc --- /dev/null +++ b/Dockerfile.aarch64 @@ -0,0 +1,126 @@ +# syntax=docker/dockerfile:1 + +# # +# @project TVApp2 +# @usage docker image which allows you to download a m3u playlist and EPG guide data from +# multiple IPTV services. +# @file Dockerfile +# @repo https://github.com/iFlip721/tvapp2 +# https://github.com/aetherinox/tvapp2 +# https://github.com/aetherinox/docker-base-alpine +# https://git.binaryninja.net/pub_projects/tvapp2 +# +# you can build your own image by running +# amd64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 -t tvapp2:1.0.0-amd64 -f Dockerfile . +# arm64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:1.0.0-arm64 -f Dockerfile.aarch64 . +# +# if you prefer to use `docker buildx` +# create docker buildx create --driver docker-container --name container --bootstrap --use +# amd64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/amd64 --output type=docker --output type=docker . +# arm64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/arm64 --output type=docker --output type=docker . +# # + +FROM ghcr.io/aetherinox/alpine-base:3.20-arm64 + +# # +# Set Args +# # + +ARG BUILDDATE +ARG VERSION + +# # +# Set Labels +# # + +LABEL maintainer="aetherinox, iFlip721" +LABEL org.opencontainers.image.authors="aetherinox, iFlip721" +LABEL org.opencontainers.image.vendor="BinaryNinja" +LABEL org.opencontainers.image.title="TvApp m3u playlist and EPG guide downloader" +LABEL org.opencontainers.image.description="Download m3u playlist and EPG guide data for the IPTV service TheTVApp" +LABEL org.opencontainers.image.source="https://git.binaryninja.net/pub_projects/tvapp2" +LABEL org.opencontainers.image.documentation="https://git.binaryninja.net/pub_projects/tvapp2/wiki" +LABEL org.opencontainers.image.url="https://git.binaryninja.net/pub_projects/tvapp2/packages" +LABEL org.opencontainers.image.licenses="MIT" +LABEL build_version="TvApp2 v${VERSION} build-date: ${BUILDDATE}" + +# # +# Set Env Var +# # + +ENV TZ="Etc/UTC" +ENV URL_REPO_BASE="https://github.com/aetherinox/alpine-base/pkgs/container/alpine-base" +ENV URL_REPO_APP="https://git.binaryninja.net/pub_projects/tvapp2" +ENV FILE_NAME="index.html" +ENV PORT_HTTP=4124 +ENV NODE_VERSION=18.20.5 +ENV YARN_VERSION=1.22.22 + +# # +# Install +# # + +RUN \ + apk add --no-cache \ + wget \ + bash \ + nano \ + npm \ + openssl + +# # +# Copy docker-entrypoint +# # + +COPY docker-entrypoint.sh /usr/local/bin/ + +# # +# Set work directory +# # + +WORKDIR /usr/src/app + +# # +# copy node package.json to workdir +# # + +COPY package*.json ./ + +# # +# install node (production) +# # + +RUN npm install --only=production + +# # +# Add local files +# # + +COPY . . +# COPY node_modules/ package.json package-lock.json formatted.dat index.js ./ + +# # +# when copying with the command above, all files in root folder will be copied. +# # + +RUN rm -rf ./root +RUN rm ./Dockerfile ./Dockerfile.aarch64 docker-entrypoint.sh + +# # +# copy s6-overlays root to image root +# # + +COPY root/ / + +# # +# Ports and volumes +# # + +EXPOSE ${PORT_HTTP}/tcp + +# # +# In case user sets up the cron for a longer duration, do a first run +# and then keep the container running. Hacky, but whatever. +# # + +CMD ["sh", "-c", "npm start"] diff --git a/LICENSE b/LICENSE index db4ed882..596b532d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,9 @@ MIT License -Gistr - Copyright (c) 2025 Aetherinox +Copyright (c) 2025 pub_projects -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 5ffa1d21..17177309 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,15 @@
-
Automatic m3u8 / xml grabber for TheTvApp
-

♾️ TheTvApp Automated Grabber ♾️

- -
- -Docker image which automatically fetches the M3U playlist and EPG (XML) guide data for TheTvApp. Can be loaded into IPTV applications such as Jellyfin. - -Makes use of the generous work over at [https://github.com/dtankdempse/thetvapp-m3u](https://github.com/dtankdempse/thetvapp-m3u) - -

- -
- - - -
-
- +
A self-hosted docker container which allows you to retrieve M3U playlists and EPG guide data from numerous online IPTV services.
+

♾️ TVApp2 ♾️


-> [!NOTE] -> Currently, the files are not updated with the latest tokens. The developer [https://github.com/dtankdempse/](https://github.com/dtankdempse/) is currently migrating to an alternative solution. -> Stay tuned. - -
-
[![Version][github-version-img]][github-version-uri] -[![Docker Version][dockerhub-version-img]][dockerhub-version-uri] [![Downloads][github-downloads-img]][github-downloads-uri] -[![Docker Pulls][dockerhub-pulls-img]][dockerhub-pulls-uri] -[![Build Status][github-build-img]][github-build-uri] [![Size][github-size-img]][github-size-img] [![Last Commit][github-commit-img]][github-commit-img] [![Contributors][contribs-all-img]](#contributors-) @@ -49,26 +24,30 @@ Makes use of the generous work over at [https://github.com/dtankdempse/thetvapp-
- [About](#about) -- [Docker Images](#docker-images) -- [Docker Tags](#docker-tags) -- [Install](#install) - - [Docker Run](#docker-run) - - [Docker Compose](#docker-compose) - - [Traefik](#traefik) - - [Dynamic.yml](#dynamicyml) - - [Static.yml](#staticyml) - - [certificatesResolvers](#certificatesresolvers) - - [entryPoints (Normal)](#entrypoints-normal) - - [entryPoints (Cloudflare)](#entrypoints-cloudflare) -- [Env Variables \& Volumes](#env-variables--volumes) - - [Environment Variables](#environment-variables) - - [Volumes](#volumes) -- [Build](#build) - - [Troubleshooting](#troubleshooting) - - [Permission Denied](#permission-denied) -- [Shell / Bash](#shell--bash) -- [SSL Certificates](#ssl-certificates) -- [Logs](#logs) +- [Building `tvapp` Image](#building-tvapp-image) + - [Before Building](#before-building) + - [LF over CRLF](#lf-over-crlf) + - [Set `+x / 0755` Permissions](#set-x--0755-permissions) + - [Build `tvapp` Image](#build-tvapp-image) + - [Option 1: Using `docker build`](#option-1-using-docker-build) + - [amd64](#amd64) + - [arm64 / aarch64](#arm64--aarch64) + - [Option 2: Using `docker buildx`](#option-2-using-docker-buildx) + - [Build \& Save Local Image](#build--save-local-image) + - [amd64](#amd64-1) + - [arm64 / aarch64](#arm64--aarch64-1) + - [Build \& Upload to Registry](#build--upload-to-registry) + - [amd64](#amd64-2) + - [arm64 / aarch64](#arm64--aarch64-2) + - [Option 3: Using `package.json`](#option-3-using-packagejson) + - [Platform Commands](#platform-commands) + - [Available Variables](#available-variables) +- [Using `tvapp` Image](#using-tvapp-image) + - [docker run](#docker-run) + - [docker-compose.yml](#docker-composeyml) +- [Extra Notes](#extra-notes) + - [Custom Docker Image Scripts](#custom-docker-image-scripts) +- [Dedication](#dedication) - [Contributors ✨](#contributors-)
@@ -78,22 +57,17 @@ Makes use of the generous work over at [https://github.com/dtankdempse/thetvapp-
## About -This container allows you to automatically fetch the latest `.m3u8` playlist, and `.xml` guide files for the TheTvApp IPTV service. -Once the container is started up, an initial grab will be done immediately. After that initial grab, the container will periodically grab new copies of the files every X hours, which can be adjusted by modifying the docker environment variables. +- TVApp2 makes fetch request to [tvapp2-externals](https://git.binaryninja.net/pub_projects/tvapp2-externals 'tvapp2-externals') making updates to external formats agnostic to pushing a new container image. +- TVApp2 makes fetch request to [XMLTV-EPG](https://git.binaryninja.net/pub_projects/XMLTV-EPG 'XMLTV-EPG') making updates to EPG data based on customized channel ids. Channel ids are specific to each EPG record which makes obfusctaing channel ids difficult. -The fetched .m3u8 and .xml files are then placed in a self-hosted nginx webserver which allows you to add the direct links directly into applications such as Jellyfin without having to go back and update the files on your own. - -
- -Container supports the following: -- Automatically grabs .m3u8 and .xml files when container started up -- Every 60 minutes, a new copy of the .m3u8 and .xml files will be fetched -- Supports both ports `80` and `443` -- Self-signed SSL certificates (optional) -- Mountable volume to control Nginx webserver files -- Customizable URLs via env var should the m3u8 and xml links change -- Integrated nginx hosted file browser for viewing all downloaded files, along with date and file size +```mermaid +graph TD +A[tvapp2] <--> |Fetch Formats| B(tvapp2-externals) +A[tvapp2] <--> |Fetch XMLTV/EPG| C(XMLTV-EPG) +B(tvapp2-externals) --> D{Pull Dynamic Formats} +C(XMLTV-EPG) ---> E{Pull Dynamic EPG} +```
@@ -101,386 +75,66 @@ Container supports the following:
-## Docker Images -Use any of the following images in your `docker-compose.yml` or `run` command: +## Building `tvapp` Image + +These instructions outline how to build your own tvapp2 docker image. When building your images with the commands provided below, ensure you create two sets of tags: + +| Architecture | Dockerfile | Tags | +| ------------ | -------------------- | ----------------------------------------------------------------------- | +| `amd64` | `Dockerfile` | `tvapp2:latest`
`tvapp2:1.0.0`
`tvapp2:1.0.0-amd64` | +| `arm64` | `Dockerfile.aarch64` | `tvapp2:1.0.0-arm64` | + +The `amd64` arch gets a few extra tags because it should be the default image people clone.
-| Service | Version | Image Link | -| --- | --- | --- | -| `Docker Hub` | [![Docker Version][dockerhub-version-ftb-img]][dockerhub-version-ftb-uri] | `aetherinox/thetvapp:latest` | -| `Github` | [![Github Version][github-version-ftb-img]][github-version-ftb-uri] | `ghcr.io/aetherinox/thetvapp-docker:latest` | +### Before Building + +Prior to building the docker image, you **must** ensure the following conditions are met. If the below tasks are not performed, your docker container will throw the following errors when started: + +- `Failed to open apk database: Permission denied` +- `s6-rc: warning: unable to start service init-adduser: command exited 127` +- `unable to exec /etc/s6-overlay/s6-rc.d/init-envfile/run: Permission denied` +- `/etc/s6-overlay/s6-rc.d/init-adduser/run: line 34: aetherxown: command not found` +- `/etc/s6-overlay/s6-rc.d/init-adduser/run: /usr/bin/aetherxown: cannot execute: required file not found`
---- +#### LF over CRLF + +You cannot utilize Windows' `Carriage Return Line Feed`. All files must be converted to Unix' `Line Feed`. This can be done with **[Visual Studio Code](https://code.visualstudio.com/)**. OR; you can run the Linux terminal command `dos2unix` to convert these files. + +If you cloned the files from the official repository **[iflip721/tvapp2](https://git.binaryninja.net/pub_projects/tvapp2)** and have not edited them, then you should not need to do this step.
-## Docker Tags -This repo includes a few different versions of the TheAppTV docker image. +> [!CAUTION] +> Be careful using the command to change **ALL** files. You should **NOT** change the files in your `.git` folder, otherwise you will corrupt your git indexes. +> +> If you accidentally run dos2unix on your `.git` folder, do NOT push anything to git. Pull a new copy from the repo.
-We release two versions, one docker image with just Nginx, and one with Nginx and PHP. The version that includes PHP is completely optional, and is only needed if you wish to develop your own enhancements for this image. - -
- -The image that comes with Nginx and PHP is slightly larger (roughly `20MB` + more). - -
- -| Tag | Includes Nginx | Includes PHP 8 | Description | -| --- | --- | --- | --- | -| `:latest` | ✅ | ❌ | Latest version of the image | -| `:1.x.x` | ✅ | ❌ | Pull a specific version | -| `:1.x.x-php` | ✅ | ✅ | Contains both Nginx and PHP; larger image size | - -
- ---- - -
- -## Install -Instructions on using this container - -
- -### Docker Run -If you want to bring the docker container up quickly, use the following command: - ```shell -docker run -d --restart=unless-stopped -e CRON_TIME=*/60 * * * * -p 443:443 --name thetvapp -v ${PWD}/thetvapp:/config ghcr.io/aetherinox/thetvapp-docker:latest +# Change ALL files +find ./ -type f | grep -Ev '.git|*.jpg|*.jpeg|*.png' | xargs dos2unix -- + +# Change run / binaries +find ./ -type f -name 'run' | xargs dos2unix -- ```
-### Docker Compose -Create a new `docker-compose.yml` with the following: - -```yml -services: - thetvapp: - container_name: thetvapp - image: ghcr.io/aetherinox/thetvapp-docker:latest # Github image - # image: aetherinox/thetvapp:latest # Dockerhub image - restart: unless-stopped - volumes: - - ./thetvapp:/config - environment: - - PUID=1000 - - PGID=1000 - - TZ=Etc/UTC - - CRON_TIME=*/60 * * * * -``` - -
- -> [!CAUTION] -> Do **not** add `"` quotation marks to `CRON_TIME` environment variable. Automated timer will not function if you do. -> -> ✔️ Correct -> ```yml -> environment: -> - CRON_TIME=*/60 * * * * -> ``` -> -> ❌ Incorrect -> ```yml -> environment: -> - CRON_TIME="*/60 * * * *" -> ``` - -
- -### Traefik -You can put this container behind Traefik if you want to use a reverse proxy and let Traefik handle the SSL certificate. - -
- -> [!NOTE] -> These steps are **optional**. -> -> If you do not use Traefik, you can skip this section of steps. This is only for users who wish to put this container behind Traefik. - -
- -#### Dynamic.yml -Open the Traefik dynamic file which is usually named `dynamic.yml`. We need to add a new `middleware`, `router`, and `service` to our Traefik dynamic file so that it knows about our new TheTVApp container and where it is. - -```yml -http: - middlewares: - https-redirect: - redirectScheme: - scheme: "https" - permanent: true - - routers: - thetvapp-http: - service: thetvapp - rule: Host(`domain.localhost`) || Host(`thetvapp.domain.com`) - entryPoints: - - http - middlewares: - - https-redirect@file - - thetvapp-https: - service: thetvapp - rule: Host(`domain.localhost`) || Host(`thetvapp.domain.com`) - entryPoints: - - https - tls: - certResolver: cloudflare - domains: - - main: "domain.com" - sans: - - "*.domain.com" - - services: - thetvapp: - loadBalancer: - servers: - - url: "https://thetvapp:443" -``` - -
- -#### Static.yml -These entries will go in your Traefik `static.yml` file. Any changes made to this file requires that you reset Traefik afterward. - -
- -##### certificatesResolvers - -Open your Traefik `static.yml` file and add your `certResolver` from above. We are going to use Cloudflare in this exmaple, you can use whatever from the list at: -- https://doc.traefik.io/traefik/https/acme/#providers - -
- -```yml -certificatesResolvers: - cloudflare: - acme: - email: youremail@address.com - storage: /cloudflare/acme.json - keyType: EC256 - preferredChain: 'ISRG Root X1' - dnsChallenge: - provider: cloudflare - delayBeforeCheck: 15 - resolvers: - - "1.1.1.1:53" - - "1.0.0.1:53" - disablePropagationCheck: true -``` - -
- -Once you pick the DNS / SSL provider you want to use from the code above, you need to see if that provider has any special environment variables that must be set. The [Providers Page](https://doc.traefik.io/traefik/https/acme/#providers) lists all providers and also what env variables need set for each one. - -
- -In our example, since we are using Cloudflare for `dnsChallenge` -> `provider`, we must set: -- `CF_API_EMAIL` -- `CF_API_KEY` - -
- -Create a `.env` environment file in the same folder where your Traefik `docker-compose.yml` file is located, and add the following: - -```yml -CF_API_EMAIL=yourcloudflare@email.com -CF_API_KEY=Your-Cloudflare-API-Key -``` - -
- -Save the `.env` file and exit. - -
- -##### entryPoints (Normal) -Finally, inside the Traefik `static.yml`, we need to make sure we have our `entryPoints` configured. Add the following to the Traefik `static.yml` file only if you **DON'T** have entry points set yet: - -```yml -entryPoints: - http: - address: :80 - http: - redirections: - entryPoint: - to: https - scheme: https - - https: - address: :443 - http3: {} - http: - tls: - options: default - certResolver: cloudflare - domains: - - main: domain.com - sans: - - '*.domain.com' -``` - -
- -##### entryPoints (Cloudflare) -If your website is behind Cloudflare's proxy service, you need to modify your `entryPoints` above so that you can automatically allow Cloudflare's IP addresses through. This means your entry points will look a bit different. - -
- -In the example below, we will add `forwardedHeaders` -> `trustedIPs` and add all of Cloudflare's IPs to the list which are available here: -- https://www.cloudflare.com/ips/ - -```yml - http: - address: :80 - forwardedHeaders: - trustedIPs: &trustedIps - - 103.21.244.0/22 - - 103.22.200.0/22 - - 103.31.4.0/22 - - 104.16.0.0/13 - - 104.24.0.0/14 - - 108.162.192.0/18 - - 131.0.72.0/22 - - 141.101.64.0/18 - - 162.158.0.0/15 - - 172.64.0.0/13 - - 173.245.48.0/20 - - 188.114.96.0/20 - - 190.93.240.0/20 - - 197.234.240.0/22 - - 198.41.128.0/17 - - 2400:cb00::/32 - - 2606:4700::/32 - - 2803:f800::/32 - - 2405:b500::/32 - - 2405:8100::/32 - - 2a06:98c0::/29 - - 2c0f:f248::/32 - http: - redirections: - entryPoint: - to: https - scheme: https - - https: - address: :443 - http3: {} - forwardedHeaders: - trustedIPs: *trustedIps - http: - tls: - options: default - certResolver: cloudflare - domains: - - main: domain.com - sans: - - '*.domain.com' -``` - -
- -Save the files and then give Traefik and your TheTvApp containers a restart. - -
- ---- - -
- -## Env Variables & Volumes -This section outlines that environment variables can be specified, and which volumes you can mount when the container is started. - -
- -### Environment Variables -The following env variables can be modified before spinning up this container: - -
- -| Env Var | Default | Description | -| --- | --- | --- | -| `PUID` | 1000 | User ID running the container | -| `PGID` | 1000 | Group ID running the container | -| `TZ` | Etc/UTC | Timezone | -| `PORT_HTTP` | 80 | Defines the HTTP port to run on | -| `PORT_HTTPS` | 443 | Defines the HTTPS port to run on | -| `CRON_TIME` | 0/60 * * * * | Determines how often the .m3u8 and .xml guide files are updated | -| `URL_XML` | https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml | URL to fetch `.xml` file | -| `URL_XML_GZ` | https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml.gz | URL to fetch `.xml.gz` file | -| `URL_M3U` | https://thetvapp-m3u.data-search.workers.dev/playlist | URL to fetch `.m3u8` file | - -
- -Please note that you can change the URLs for the files fetched from the internet, but it is highly advised to not do this unless you know for sure that the location paths have changed. To change the URLs to the `m3u8`, `.xml`, and `.xml.gz`; change the following environment variables: - -- `URL_XML=https://url/to/file.xml` -- `URL_XML_GZ=https://url/to/file.xml.gz` -- `URL_M3U=https://url/to/file.m3u8` - -
- -### Volumes -The following volumes can be mounted with this container: - -
- -| Volume | Description | -| --- | --- | -| `./thetvapp:/config` | Path which stores downloaded `.m3u8`, `.xml`, nginx configs, and optional SSL certificate/keys | - -
- -By mounting the volume above, you should now have access to the following folders: -
- -| Folder | Description | -| --- | --- | -| `📁 keys` | Responsible for storing your ssl certificate `cert.crt` + key `cert.key` | -| `📁 log` | All nginx and php logs | -| `📁 nginx` | Contains `nginx.conf`, `resolver.conf`, `ssl.conf`, `site-confs` | -| `📁 php` | Contains `php-local.ini`, `www2.conf` | -| `📁 www` | Folder where downloaded `.m3u8`, `.xml`, and `.xml.gz` will be downloaded to | - -
- ---- - -
- -## Build -You can build your own copy of the image by running the following: +#### Set `+x / 0755` Permissions +The files contained within this repo **MUST** have `chmod 755` / `+x` executable permissions. ```shell -git clone https://github.com/Aetherinox/thetvapp-docker.git . -docker build -t thetvapp:latest thetvapp:1.0.0 . +find ./ -name 'run' -exec sudo chmod +x {} \; ```
-### Troubleshooting -These are issues you may experience when building and deploying your own custom image. - -
- -#### Permission Denied - -```console -Failed to open apk database: Permission denied -unable to exec /etc/s6-overlay/s6-rc.d/init-envfile/run: Permission denied -unable to exec /etc/s6-overlay/s6-rc.d/init-envfile/run: Permission denied -``` - -
- -If you receive any type of `permission denied` error when running your custom image, you must ensure that certain files have executable `+x` (or `0755`) permissions. Once you fix the file permissions, re-build the image. A full list of files requiring elevated permissions are listed below: +**[Optional]**: If you want to set the permissions manually, run the following below. If you executed the `find` command above, you don't need to run the list of commands below: ```shell sudo chmod +x /root/etc/s6-overlay/s6-rc.d/init-adduser/run @@ -498,8 +152,303 @@ sudo chmod +x /root/etc/s6-overlay/s6-rc.d/init-version-checks/run sudo chmod +x /root/etc/s6-overlay/s6-rc.d/svc-cron/run sudo chmod +x /root/etc/s6-overlay/s6-rc.d/svc-nginx/run sudo chmod +x /root/etc/s6-overlay/s6-rc.d/svc-php-fpm/run -sudo chmod +x /run.sh -sudo chmod +x /download.sh +``` + +
+ +### Build `tvapp` Image +After completing the items above, you can now build the **[iflip721/tvapp2](https://git.binaryninja.net/pub_projects/tvapp2)** image. You can now build the TvApp2 docker image. Pick your platform below and run the associated command. Most people will want to use [amd64](#amd64). + +
+ +Instructions have been provided below on two different ways you can build the TvApp2 docker image. You can use either one, it depends on what tools you have available on the system you're. + +- [Using docker build commands](#option-1-using-docker-build) +- [Using docker buildx commands](#option-2-using-docker-buildx) +- [Using available node commands](#option-3-using-packagejson) + +
+ +#### Option 1: Using `docker build` + +This method will show you how to build the TVApp2 docker image using `docker build`; this is typically what most people should use. + +
+ +##### amd64 + +```shell ignore +# Build tvapp2 amd64 +docker build --network=host --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250224 -t tvapp2:latest -t tvapp2:1.0.0 -t tvapp2:1.0.0-amd64 -f Dockerfile . +``` + +
+ +##### arm64 / aarch64 + +```shell ignore +# Build tvapp2 arm64 +docker build --network=host --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250224 -t tvapp2:1.0.0-arm64 -f Dockerfile.aarch64 . +``` + +
+
+ +#### Option 2: Using `docker buildx` + +This section explains how to build the TVApp2 docker image using `docker buildx` instead of `docker build`. It is useful when generating your app's image for multiple platforms. + +
+ +All of the needed Docker files already exist in the repository. To get started, clone the repo to a folder + +```shell ignore +mkdir tvapp2 && cd tvapp2 + +# to clone from our gitea website +git clone https://git.binaryninja.net/pub_projects/tvapp2.git ./ + +# to clone from our github website +git clone https://github.com/iFlip721/tvapp2.git ./ +``` + +
+ +Once the files are downloaded, create a new container for **buildx** + +```shell ignore +docker buildx create --driver docker-container --name container --bootstrap --use +``` + +
+ +**Optional** If you have previously created this image and have not restarted your system, clean up the original container before you build again: + +```shell ignore +docker buildx rm container + +docker buildx create --driver docker-container --name container --bootstrap --use +``` + +
+ +You are now ready to build the TVApp2 docker image. Two different options are provided below: +- **Option 1:** [Build & Save Local Image](#build--save-local-image) + - Use this option if you only wish to build the image and use it. +- **Option 2:** [Build & Upload to Registry](#build--upload-to-registry) + - Use this option if you wish to build the image and publish it to a registry online for others to use. + +
+ +##### Build & Save Local Image +The command below will build your TVApp2 docker image, and save a local copy of your docker app, which can be immediately used, or seen using `docker ps`. + +
+ + +###### amd64 +```shell ignore +# Build tvapp2 amd64 +docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250224 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/amd64 --output type=docker --output type=docker . +``` + +
+ +###### arm64 / aarch64 +```shell ignore +# Build tvapp2 arm64 +docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250224 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/arm64 --output type=docker --output type=docker . +``` + +
+ +If we list our docker images, we should see our new one: + +``` +$ docker images + +tvapp2 1.0.0 122e9b2c6046 1 minute ago 107MB +tvapp2 1.0.0-amd64 122e9b2c6046 1 minute ago 107MB +tvapp2 latest 122e9b2c6046 1 minute ago 107MB +``` + +
+
+ +##### Build & Upload to Registry + +This option builds your TVApp2 docker image, and then pushes the new image to a registry such as hub.docker.com or Github's registry ghcr. + +Before you can push the image, ensure you are signed into Docker CLI. Open your Linux terminal and see if you are already signed in: + +```shell ignore +docker info | grep Username +``` + +
+ +If nothing is printed; then you are not signed in. Initiate the web login: + +```shell ignore +docker login +``` + +
+ +Some text will appear on-screen, copy the code, open your browser, and go to https://login.docker.com/activate + +```console +USING WEB BASED LOGIN +To sign in with credentials on the command line, use 'docker login -u ' + +Your one-time device confirmation code is: XXXX-XXXX +Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate + +Waiting for authentication in the browser… +``` + +
+ +Once you are finished in your browser, you can return to your Linux terminal, and it should bring you back to where you can type a command. You can now verify again if you are signed in: + +```shell ignore +docker info | grep Username +``` + +
+ +You should see your name: +```console + Username: Aetherinox +``` + +
+ +You are ready to build the TVApp2 docker image, run the command for your platform: + +###### amd64 + +```shell ignore +docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250224 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/amd64 --provenance=true --sbom=true --builder=container --push . +``` + +###### arm64 / aarch64 + +```shell ignore +docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250224 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/arm64 --provenance=true --sbom=true --builder=container --push . +``` + +
+
+ +#### Option 3: Using `package.json` + +This node project includes build commands. In order to use them you must install node on your machine. + +```shell ignore +sudo apt-get install node +``` + +
+ +To build the project, `cd` into the project folder and run the build command: + +```shell ignore +cd /home/docker/tvapp2/ + +npm run docker:build:amd64 --VERSION=1.0.1 --BUILDDATE=20250220 +``` + +
+ +##### Platform Commands + +The following is a list of the available commands you can pick from depending on how you would like to build TvAPP2: + +| Command | Description | +| --- | --- | +| `docker:build:amd64` | Build image using `docker build` for `amd64` | +| `docker:build:arm64` | Build image using `docker build` for `arm64 / aarch64` | +| `docker:buildx:amd64` | Build image using `docker buildx` for `amd64` | +| `docker:buildx:arm64` | Build image using `docker buildx` for `arm64 / aarch64` | + +
+ +##### Available Variables +The run command above has several variables you must specify: + +| Variable | Description | +| --- | --- | +| `--VERSION=1.X.X` | The version to assign to the docker image | +| `--BUILDDATE=20250220` | The date to assign to the docker image.
Date format: `YEAR / MONTH / DAY` | + + +
+ +--- + +
+ +## Using `tvapp` Image +To use the new TVApp2 image, you can either call it with the `docker run` command, or create a new `docker-compose.yml` and specify the image: + +
+ +### docker run +If you want to use the tvapp docker image in the `docker run` command, execute the following: +```shell ignore +docker run -d --restart=unless-stopped -p 4124:4124 --name tvapp2 -v ${PWD}/tvapp:/config ghcr.io/iflip721/tvapp2:latest +``` + +
+ +### docker-compose.yml +If you'd much rather use a `docker-compose.yml` file and call the tvapp image that way, create a new folder somewhere: +```shell ignore +mkdir -p /home/docker/tvapp2 +``` + +
+ +Then create a new `docker-compose.yml` file and add the following: +```shell ignore +sudo nano /home/docker/tvapp2/docker-compose.yml +``` + +
+ +Add the following to your `docker-compose.yml`: + +```yml ignore +services: + tvapp: + container_name: tvapp2 + image: ghcr.io/iflip721/tvapp2:latest # Github image + # image: iflip721/tvapp:latest # Dockerhub image + restart: unless-stopped + volumes: + - ./tvapp:/config + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC +``` + +
+ +Once the `docker-compose.yml` is set up, you can now start your TVApp2 container: + +```shell ignore +cd /home/docker/tvapp2/ +docker compose up -d +``` + +
+ +TVApp2 should now be running as a container. You can access it by opening your browser and going to: + +```shell ignore +http://container-ip:4124 ```
@@ -508,51 +457,64 @@ sudo chmod +x /download.sh
-## Shell / Bash -You can access the docker container's shell by running: +## Extra Notes + +The following are other things to take into consideration when creating the TVApp2 image: + +
+ +### Custom Docker Image Scripts + +These instructions are for **Advanced Users Only** + +The `🔀 iflip721/tvapp2` image supports the ability of adding custom scripts that will be ran when the container is started. To create / add a new custom script to the container, you need to create a new folder in the container source files `/root` folder ```shell -docker exec -it thetvapp ash +mkdir -p /root/custom-cont-init.d/ ```
---- +Within this new folder, add your custom script: + +```shell +nano /root/custom-cont-init.d/my_customs_script +```
-## SSL Certificates -This docker image automatically generates an SSL certificate when the nginx server is brought online. +Your new custom script should be populated with the bash code you want to perform actions with such as the example below: + +```bash +#!/bin/bash + +echo "**** INSTALLING BASH ****" +apk add --no-cache bash +```
-

- -
- -You may opt to either use the generated self-signed certificate, or you can add your own. If you decide to use your own self-signed certificate, ensure you have mounted the `/config` volume in your `docker-compose.yml`: +When you create the docker image, this new script will automatically be loaded. You can also do this via the `📄 docker-compose.yml` file by mounting a new volume: ```yml services: - thetvapp: - container_name: thetvapp - image: ghcr.io/aetherinox/thetvapp-docker:latest # Github image - restart: unless-stopped + tvapp2: volumes: - - ./thetvapp:/config + - ./tvapp2:/config + - ./custom-scripts:/custom-cont-init.d:ro ```
-Then navigate to the newly mounted folder and add your `📄 cert.crt` and `🔑 cert.key` files to the `📁 /thetvapp/keys/*` folder. - -
- > [!NOTE] -> If you are generating your own certificate and key, we recommend a minimum of: -> - RSA: `2048 bits` -> - ECC: `256 bits` -> - ECDSA: `P-384 or P-521` +> if using compose, we recommend mounting them **read-only** (`:ro`) so that container processes cannot write to the location. + +> [!WARNING] +> The folder `📂 /root/custom-cont-init.d` **MUST** be owned by `root`. If this is not the case, this folder will be renamed and a new empty folder will be created. This is to prevent remote code execution by putting scripts in the aforesaid folder. + +
+ +The `🔀 iflip721/tvapp2` image already contains a custom script called `📄 /root/custom-cont-init.d/plugins`. Do **NOT** edit this script. It is what automatically downloads the official TVApp2 plugins and adds them to the container.
@@ -560,42 +522,9 @@ Then navigate to the newly mounted folder and add your `📄 cert.crt` and `🔑
-## Logs -This image spits out detailed information about its current progress. You can either use `docker logs` or a 3rd party app such as [Portainer](https://portainer.io/) to view the logs. +## Dedication -
- -```shell -─────────────────────────────────────────────────────────────── - TheTvApp Docker Container -─────────────────────────────────────────────────────────────── - - This container automatically downloads the m3u8 and xml guide - data from - - https://github.com/dtankdempse/thetvapp-m3u - - Once the data is downloaded, you can access the files from - the container's webserver. - - User ID ........... 1000 - Group ID .......... 1000 - Port HTTP ......... 80 - Port HTTPS ........ 443 - -─────────────────────────────────────────────────────────────── - - SSL : Using existing keys found in /config/keys - Loader : No custom files found, skipping... - Core : Completed loading container - Config : Setting task to run */60 * * * * - Setting timezone Etc/UTC - - Start : Downloading latest thetvapp m3u + xml - Getting thetvapp.xml › https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml - Getting thetvapp.xml.gz › https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml.gz - Getting thetvapp.m3u8 › https://thetvapp-m3u.data-search.workers.dev/playlist - End : Finished update at 12-01-2024 15:00:00 -``` +This repository and this project serves in memory of the developer [dtankdemp](https://hub.docker.com/r/dtankdemp). His work lives on in this project, and while a lot of it has changed, it all started because of him.
@@ -612,15 +541,11 @@ We are always looking for contributors. If you feel that you can provide somethi
Want to help but can't write code? -- Review [active questions by our community](https://github.com/Aetherinox/thetvapp-docker/labels/help%20wanted) and answer the ones you know. +- Review [active questions by our community](https://github.com/iFlip721/tvapp2/labels/help%20wanted) and answer the ones you know.
-
- -![Alt](https://repobeats.axiom.co/api/embed/84970e7951598969bbe3291ae29e352837721cad.svg "analytics image") - -
+![Alt](https://repobeats.axiom.co/api/embed/16789a0ce9d38f369b00eb5c337fc8eb72110f4b.svg "Analytics image")
@@ -638,12 +563,19 @@ The following people have helped get this project going: - - - - - - + + + + + + +
Aetherinox
Aetherinox

💻 📆 🔍
dtankdempse
dtankdempse

🔧
+ Aetherinox
Aetherinox

💻 +
+ iFlip721
iFlip721

💻 +
+ Nvmdfth
Optx

💻 +
@@ -653,86 +585,43 @@ The following people have helped get this project going:

- - - [general-npmjs-uri]: https://npmjs.com [general-nodejs-uri]: https://nodejs.org - [general-npmtrends-uri]: http://npmtrends.com/thetvapp-docker + [general-npmtrends-uri]: http://npmtrends.com/csf-firewall - [github-version-img]: https://img.shields.io/github/v/tag/Aetherinox/thetvapp-docker?logo=GitHub&label=Version&color=ba5225 - [github-version-uri]: https://github.com/Aetherinox/thetvapp-docker/releases - - - [github-version-ftb-img]: https://img.shields.io/github/v/tag/Aetherinox/thetvapp-docker?style=for-the-badge&logo=github&logoColor=FFFFFF&logoSize=34&label=%20&color=ba5225 - [github-version-ftb-uri]: https://github.com/Aetherinox/thetvapp-docker/releases - - - [npm-version-img]: https://img.shields.io/npm/v/thetvapp-docker?logo=npm&label=Version&color=ba5225 - [npm-version-uri]: https://npmjs.com/package/thetvapp-docker - - - [pypi-version-img]: https://img.shields.io/pypi/v/thetvapp-docker-plugin - [pypi-version-uri]: https://pypi.org/project/thetvapp-docker-plugin/ + [github-version-img]: https://img.shields.io/github/v/tag/iFlip721/tvapp2?logo=GitHub&label=Version&color=ba5225 + [github-version-uri]: https://github.com/iFlip721/tvapp2/releases [license-mit-img]: https://img.shields.io/badge/MIT-FFF?logo=creativecommons&logoColor=FFFFFF&label=License&color=9d29a0 - [license-mit-uri]: https://github.com/Aetherinox/thetvapp-docker/blob/main/LICENSE + [license-mit-uri]: https://github.com/iFlip721/tvapp2/blob/main/LICENSE - [github-downloads-img]: https://img.shields.io/github/downloads/Aetherinox/thetvapp-docker/total?logo=github&logoColor=FFFFFF&label=Downloads&color=376892 - [github-downloads-uri]: https://github.com/Aetherinox/thetvapp-docker/releases - - - [npmjs-downloads-img]: https://img.shields.io/npm/dw/%40aetherinox%2Fcsf-firewall?logo=npm&&label=Downloads&color=376892 - [npmjs-downloads-uri]: https://npmjs.com/package/thetvapp-docker + [github-downloads-img]: https://img.shields.io/github/downloads/iFlip721/tvapp2/total?logo=github&logoColor=FFFFFF&label=Downloads&color=376892 + [github-downloads-uri]: https://github.com/iFlip721/tvapp2/releases - [github-size-img]: https://img.shields.io/github/repo-size/Aetherinox/thetvapp-docker?logo=github&label=Size&color=59702a - [github-size-uri]: https://github.com/Aetherinox/thetvapp-docker/releases - - - [npmjs-size-img]: https://img.shields.io/npm/unpacked-size/thetvapp-docker/latest?logo=npm&label=Size&color=59702a - [npmjs-size-uri]: https://npmjs.com/package/thetvapp-docker - - - [codecov-coverage-img]: https://img.shields.io/codecov/c/github/Aetherinox/thetvapp-docker?token=MPAVASGIOG&logo=codecov&logoColor=FFFFFF&label=Coverage&color=354b9e - [codecov-coverage-uri]: https://codecov.io/github/Aetherinox/thetvapp-docker + [github-size-img]: https://img.shields.io/github/repo-size/iFlip721/tvapp2?logo=github&label=Size&color=59702a + [github-size-uri]: https://github.com/iFlip721/tvapp2/releases - [contribs-all-img]: https://img.shields.io/github/all-contributors/Aetherinox/thetvapp-docker?logo=contributorcovenant&color=de1f6f&label=contributors + [contribs-all-img]: https://img.shields.io/github/all-contributors/iFlip721/tvapp2?logo=contributorcovenant&color=de1f6f&label=contributors [contribs-all-uri]: https://github.com/all-contributors/all-contributors - [github-build-img]: https://img.shields.io/github/actions/workflow/status/Aetherinox/thetvapp-docker/deploy-docker.yml?logo=github&logoColor=FFFFFF&label=Build&color=%23278b30 - [github-build-uri]: https://github.com/Aetherinox/thetvapp-docker/actions/workflows/deploy-docker.yml + [github-build-img]: https://img.shields.io/github/actions/workflow/status/iFlip721/tvapp2/npm-release.yml?logo=github&logoColor=FFFFFF&label=Build&color=%23278b30 + [github-build-uri]: https://github.com/iFlip721/tvapp2/actions/workflows/npm-release.yml - [github-build-pypi-img]: https://img.shields.io/github/actions/workflow/status/Aetherinox/thetvapp-docker/release-pypi.yml?logo=github&logoColor=FFFFFF&label=Build&color=%23278b30 - [github-build-pypi-uri]: https://github.com/Aetherinox/thetvapp-docker/actions/workflows/pypi-release.yml + [github-build-pypi-img]: https://img.shields.io/github/actions/workflow/status/iFlip721/tvapp2/release-pypi.yml?logo=github&logoColor=FFFFFF&label=Build&color=%23278b30 + [github-build-pypi-uri]: https://github.com/iFlip721/tvapp2/actions/workflows/pypi-release.yml - [github-tests-img]: https://img.shields.io/github/actions/workflow/status/Aetherinox/thetvapp-docker/npm-tests.yml?logo=github&label=Tests&color=2c6488 - [github-tests-uri]: https://github.com/Aetherinox/thetvapp-docker/actions/workflows/npm-tests.yml + [github-tests-img]: https://img.shields.io/github/actions/workflow/status/iFlip721/tvapp2/npm-tests.yml?logo=github&label=Tests&color=2c6488 + [github-tests-uri]: https://github.com/iFlip721/tvapp2/actions/workflows/npm-tests.yml - [github-commit-img]: https://img.shields.io/github/last-commit/Aetherinox/thetvapp-docker?logo=conventionalcommits&logoColor=FFFFFF&label=Last%20Commit&color=313131 - [github-commit-uri]: https://github.com/Aetherinox/thetvapp-docker/commits/main/ - - - [dockerhub-version-img]: https://img.shields.io/docker/v/aetherinox/thetvapp/latest?logo=docker&logoColor=FFFFFF&label=Docker%20Version&color=ba5225 - [dockerhub-version-uri]: https://hub.docker.com/repository/docker/aetherinox/thetvapp/general - - - [dockerhub-version-ftb-img]: https://img.shields.io/docker/v/aetherinox/thetvapp/latest?style=for-the-badge&logo=docker&logoColor=FFFFFF&logoSize=34&label=%20&color=ba5225 - [dockerhub-version-ftb-uri]: https://hub.docker.com/repository/docker/aetherinox/thetvapp/tags - - - [dockerhub-pulls-img]: https://img.shields.io/docker/pulls/aetherinox/thetvapp?logo=docker&logoColor=FFFFFF&label=Docker%20Pulls&color=af9a00 - [dockerhub-pulls-uri]: https://hub.docker.com/repository/docker/aetherinox/thetvapp/general - - - - + [github-commit-img]: https://img.shields.io/github/last-commit/iFlip721/tvapp2?logo=conventionalcommits&logoColor=FFFFFF&label=Last%20Commit&color=313131 + [github-commit-uri]: https://github.com/iFlip721/tvapp2/commits/main/ diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 00000000..7acdf94c --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# # +# @project TVApp2 +# @usage docker image which allows you to download a m3u playlist and EPG guide data from +# multiple IPTV services. +# @file Dockerfile +# @repo https://github.com/iFlip721/tvapp2 +# https://github.com/aetherinox/tvapp2 +# https://github.com/aetherinox/docker-base-alpine +# https://git.binaryninja.net/pub_projects/tvapp2 +# +# you can build your own image by running +# amd64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 -t tvapp2:1.0.0-amd64 -f Dockerfile . +# arm64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:1.0.0-arm64 -f Dockerfile.aarch64 . +# +# if you prefer to use `docker buildx` +# create docker buildx create --driver docker-container --name container --bootstrap --use +# amd64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/amd64 --output type=docker --output type=docker . +# arm64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/arm64 --output type=docker --output type=docker . +# # + +set -e + +# Run command with node if the first argument contains a "-" or is not a system command. The last +# part inside the "{}" is a workaround for the following bug in ash/dash: +# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=874264 +if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then + set -- node "$@" +fi + +exec "$@" diff --git a/docs/img/002.png b/docs/img/002.png deleted file mode 100644 index d31b86a4..00000000 Binary files a/docs/img/002.png and /dev/null differ diff --git a/docs/img/banner-sq.png b/docs/img/banner-sq.png deleted file mode 100644 index 85f31c55..00000000 Binary files a/docs/img/banner-sq.png and /dev/null differ diff --git a/docs/img/banner.png b/docs/img/banner.png deleted file mode 100644 index d4c56fc1..00000000 Binary files a/docs/img/banner.png and /dev/null differ diff --git a/download.sh b/download.sh deleted file mode 100755 index 419dcbeb..00000000 --- a/download.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/with-contenv bash -# shellcheck shell=bash - -# # -# @project thetvapp-docker -# @about download script for fetching m3u8 and xml -# @file /download.sh -# @repo https://github.com/Aetherinox/thetvapp-docker -# # - -DATE=$(TZ=${TZ} date '+%m-%d-%Y %H:%M:%S') - -# # -# Run Download -# # - -echo -e -echo -e " Start : Downloading latest ${FILE_NAME} m3u + xml" - -# Download .xml -wget -q -O /config/www/${FILE_NAME}.xml ${URL_XML} -echo -e " Getting ${FILE_NAME}.xml › ${URL_XML}" - -# Download .xml.gz -wget -q -O /config/www/${FILE_NAME}.xml.gz ${URL_XML_GZ} -echo -e " Getting ${FILE_NAME}.xml.gz › ${URL_XML_GZ}" - -# Download .m3u8 -wget -q -O /config/www/${FILE_NAME}.m3u8 ${URL_M3U} -echo -e " Getting ${FILE_NAME}.m3u8 › ${URL_M3U}" - -echo -e " End : Finished update at ${DATE}" diff --git a/index.js b/index.js new file mode 100644 index 00000000..ca5bf4c9 --- /dev/null +++ b/index.js @@ -0,0 +1,658 @@ +const fs = require('fs'); +const https = require('https'); +const path = require('path'); +const UserAgent = require('user-agents'); +const http = require('http'); +const os = require('os'); +const zlib = require('zlib'); +const { randomUUID } = require('crypto'); +const { channel } = require('diagnostics_channel'); +const cache = new Map(); + +let URLS_FILE; +let FORMATTED_FILE; +let EPG_FILE; +const xmltv_epg = 'https://git.binaryninja.net/pub_projects/XMLTV-EPG/raw/branch/main/xmltv.1.xml'; +const externalURL = 'https://git.binaryninja.net/pub_projects/tvapp2-externals/raw/branch/main/urls.txt'; +const externalEPG = 'https://git.binaryninja.net/pub_projects/XMLTV-EPG/raw/branch/main/xmltv.1.xml'; +const externalFORMATTED_1 = 'https://git.binaryninja.net/pub_projects/tvapp2-externals/raw/branch/main/formatted.dat'; +const externalFORMATTED_2 = ''; +const externalFORMATTED_3 = ''; +const externalEvents = ''; + +if (process.pkg) { + console.log('Process package'); + const basePath = path.dirname(process.execPath); + URLS_FILE = path.join(basePath, 'urls.txt'); + FORMATTED_FILE = path.join(basePath, 'formatted.dat'); + //EPG_FILE = path.join(basePath, 'epg.xml'); + EPG_FILE = path.join(basePath, 'xmltv.1.xml'); + EPG_FILE.length; +} else { + console.log('Process locals'); + URLS_FILE = path.resolve(__dirname, 'urls.txt'); + FORMATTED_FILE = path.resolve(__dirname, 'formatted.dat'); + EPG_FILE = path.resolve(__dirname, 'xmltv.1.xml'); +} + +class Semaphore { + constructor(max) { + this.max = max; + this.queue = []; + this.active = 0; + } + async acquire() { + if (this.active < this.max) { + this.active++; + return; + } + return new Promise((resolve) => this.queue.push(resolve)); + } + release() { + this.active--; + if (this.queue.length > 0) { + const resolve = this.queue.shift(); + this.active++; + resolve(); + } + } +} + +const semaphore = new Semaphore(5); + +let urls = []; +let tokenData = { + subdomain: null, + token: null, + url: null, + validationUrl: null, + cookies: null, +}; +let lastTokenFetchTime = 0; + +const log = (message) => { + const now = new Date(); + console.log(`[${now.toLocaleTimeString()}] ${message}`); +}; + +async function downloadFile(url, filePath) { + console.log(`Fetching ${url}`); + return new Promise((resolve, reject) => { + const isHttps = new URL(url).protocol === 'https:'; + const httpModule = isHttps ? require('https') : require('http'); + const file = fs.createWriteStream(filePath); + + httpModule + .get(url, (response) => { + if (response.statusCode !== 200) { + console.error(`Failed to download file: ${url}. Status code: ${response.statusCode}`); + return reject(new Error(`Failed to download file: ${url}. Status code: ${response.statusCode}`)); + } + response.pipe(file); + file.on('finish', () => { + log(`Sucess: ${filePath}`); + file.close(() => resolve(true)); + }); + }) + .on('error', (err) => { + console.error(`Error downloading file: ${url}. Error: ${err.message}`); + fs.unlink(filePath, () => reject(err)); + }); + }); +} + +async function ensureFileExists(url, filePath) { + try { + await downloadFile(url, filePath); + } catch (error) { + if (fs.existsSync(filePath)) { + console.warn(`Using existing file for ${filePath} due to download failure.`); + } else { + console.error(`Critical: Failed to download ${url}, and no local file exists.`); + throw error; + } + } +} + +// REMOVED REFERENCE CALLS TO THIS FUNCTION +// TODO: UPDATES TO HANDLER FOR SPORT EVENTS +async function fetchSportsData() { + return new Promise((resolve, reject) => { + const isHttps = new URL(externalEvents).protocol === 'https:'; + const httpModule = isHttps ? require('https') : require('http'); + httpModule + .get(url, (response) => { + if (response.statusCode !== 200) { + console.error(`Failed to fetch sports data. Status code: ${response.statusCode}`); + return reject(new Error(`Failed to fetch sports data. Status code: ${response.statusCode}`)); + } + let data = ''; + response.on('data', (chunk) => (data += chunk)); + response.on('end', () => { + log('Fetched sports data successfully.'); + resolve(data); + }); + }) + .on('error', (err) => { + console.error(`Error fetching sports data: ${err.message}`); + reject(err); + }); + }); +} + +async function fetchRemote(url) { + return new Promise((resolve, reject) => { + const mod = url.startsWith('https') ? https : http; + mod + .get(url, { headers: { 'Accept-Encoding': 'gzip, deflate, br' } }, (resp) => { + if (resp.statusCode !== 200) { + return reject(new Error(`HTTP ${resp.statusCode} for ${url}`)); + } + const chunks = []; + resp.on('data', (chunk) => chunks.push(chunk)); + resp.on('end', () => { + const buffer = Buffer.concat(chunks); + const encoding = resp.headers['content-encoding']; + if (encoding === 'gzip') { + zlib.gunzip(buffer, (err, decoded) => { + if (err) return reject(err); + resolve(decoded); + }); + } else if (encoding === 'deflate') { + zlib.inflate(buffer, (err, decoded) => { + if (err) return reject(err); + resolve(decoded); + }); + } else if (encoding === 'br') { + zlib.brotliDecompress(buffer, (err, decoded) => { + if (err) return reject(err); + resolve(decoded); + }); + } else { + resolve(buffer); + } + }); + }) + .on('error', reject); + }); +} + +async function serveKey(req, res) { + try { + const uriParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('uri'); + if (!uriParam) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + return res.end('Error: Missing "uri" parameter for key download.'); + } + const keyData = await fetchRemote(uriParam); + res.writeHead(200, { 'Content-Type': 'application/octet-stream' }); + res.end(keyData); + } catch (err) { + console.error('Error in serveKey:', err.message); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error fetching key.'); + } +} + +let gCookies = {}; +const USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; + +function parseSetCookieHeaders(setCookieValues) { + if (!Array.isArray(setCookieValues)) return; + setCookieValues.forEach((line) => { + const [cookiePair] = line.split(';'); + if (cookiePair) { + const [key, val] = cookiePair.split('='); + if (key && val) { + gCookies[key.trim()] = val.trim(); + } + } + }); +} + +function buildCookieHeader() { + const pairs = []; + for (const [k, v] of Object.entries(gCookies)) { + pairs.push(`${k}=${v}`); + } + return pairs.join('; '); +} + +function fetchPage(url) { + return new Promise((resolve, reject) => { + const opts = { + method: 'GET', + headers: { + 'User-Agent': USERAGENT, + Accept: '*/*', + Cookie: buildCookieHeader(), + }, + }; + https + .get(url, opts, (res) => { + if (res.statusCode !== 200) { + return reject(new Error(`Non-200 status ${res.statusCode} => ${url}`)); + } + if (res.headers['set-cookie']) { + parseSetCookieHeaders(res.headers['set-cookie']); + } + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => resolve(data)); + }) + .on('error', reject); + }); +} + +async function getTokenizedUrl(channelUrl) { + try { + const html = await fetchPage(channelUrl); + + let streamName; + let streamHost; + if (channelUrl.includes('espn-')) { + streamName = 'ESPN'; + } else if (channelUrl.includes('espn2-')) { + streamName = 'ESPN2'; + } else { + const streamNameMatch = html.match(/id="stream_name" name="([^"]+)"/); + if (!streamNameMatch) { + log('No "stream_name" found'); + return null; + } + streamName = streamNameMatch[1]; + } + if (channelUrl.match('tvpass\.org')) { + streamHost = 'tvpass.org'; + }; + if (channelUrl.match('thetvapp\.to')) { + streamHost = 'thetvapp.to'; + }; + const tokenUrl = `https://${streamHost}/token/${streamName}?quality=hd`; + const tokenResponse = await fetchPage(tokenUrl); + let finalUrl; + try { + const json = JSON.parse(tokenResponse); + finalUrl = json.url; + } catch (err) { + log('Failed to parse token JSON'); + return null; + } + if (!finalUrl) { + log('No URL found in the token JSON'); + return null; + } + log(`Tokenized URL: ${finalUrl}`); + return finalUrl; + } catch (err) { + log(`Fatal error fetching token: ${err.message}`); + return null; + } +} + +async function serveChannelPlaylist(req, res) { + await semaphore.acquire(); + try { + const urlParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('url'); + if (!urlParam) { + log('Error: Missing URL parameter'); + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Error: Missing URL parameter.'); + return; + } + const decodedUrl = decodeURIComponent(urlParam); + if (decodedUrl.endsWith('.ts')) { + res.writeHead(302, { Location: decodedUrl }); + res.end(); + return; + } + const cachedUrl = getCache(decodedUrl); + if (cachedUrl) { + const rewrittenPlaylist = await rewritePlaylist(cachedUrl, req); + res.writeHead(200, { + 'Content-Type': 'application/vnd.apple.mpegurl', + 'Content-Disposition': 'inline; filename="playlist.m3u8"', + }); + res.end(rewrittenPlaylist); + return; + } + log(`Fetching stream: ${urlParam}`); + const finalUrl = await getTokenizedUrl(decodedUrl); + if (!finalUrl) { + log('Error: Failed to retrieve tokenized URL'); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error: Failed to retrieve tokenized URL.'); + return; + } + setCache(decodedUrl, finalUrl, 4 * 60 * 60 * 1000); + const hdUrl = finalUrl.replace('tracks-v2a1', 'tracks-v1a1'); + const rewrittenPlaylist = await rewritePlaylist(hdUrl, req); + res.writeHead(200, { + 'Content-Type': 'application/vnd.apple.mpegurl', + 'Content-Disposition': 'inline; filename="playlist.m3u8"', + }); + res.end(rewrittenPlaylist); + log('Served playlist'); + } catch (error) { + log(`Error processing request: ${error.message}`); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error processing request.'); + } + } finally { + semaphore.release(); + } +} + +async function rewritePlaylist(originalUrl, req) { + const rawData = await fetchRemote(originalUrl); + const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); + const host = req.headers.host; + const baseUrl = `${protocol}://${host}`; + const playlistContent = rawData.toString('utf8'); + return playlistContent + .replace(/URI="([^"]+)"/g, (match, uri) => { + const resolvedUri = new URL(uri, originalUrl).href; + return `URI="${baseUrl}/key?uri=${encodeURIComponent(resolvedUri)}"`; + }) + .replace(/^([^#].*\.m3u8)(\?.*)?$/gm, (match, uri) => { + const resolvedUri = new URL(uri, originalUrl).href; + return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`; + }) + .replace(/^([^#].*\.ts)(\?.*)?$/gm, (match, uri) => { + const resolvedUri = new URL(uri, originalUrl).href; + return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`; + }); +} + +async function servePlaylist(response, req) { + + try { + + const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); + const host = req.headers.host; + const baseUrl = `${protocol}://${host}`; + const formattedContent = fs.readFileSync(FORMATTED_FILE, 'utf-8'); + const updatedContent = formattedContent + .replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => { + return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; + }) + .replace(/(https?:\/\/[^\s]*tvpass[^\s]*)/g, (fullUrl) => { + return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; + }); + + response.writeHead(200, { + 'Content-Type': 'application/x-mpegURL', + 'Content-Disposition': 'inline; filename="playlist.m3u8"', + }); + response.end(updatedContent); + + } catch (error) { + + console.error('Error in servePlaylist:', error.message); + response.writeHead(500, { 'Content-Type': 'text/plain' }); + response.end(`Error serving playlist: ${error.message}`); + + } + +} + +async function serveXmltv(response, req) { + + try { + + const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); + const host = req.headers.host; + const baseUrl = `${protocol}://${host}`; + const formattedContent = fs.readFileSync(EPG_FILE, 'utf-8'); + + response.writeHead(200, { + 'Content-Type': 'application/xml', + 'Content-Disposition': 'inline; filename="xmltv.1.xml"', + }); + response.end(formattedContent); + + } catch (error) { + + console.error('Error in servePlaylist:', error.message); + response.writeHead(500, { 'Content-Type': 'text/plain' }); + response.end(`Error serving playlist: ${error.message}`); + + } + +}; + +/* +ORIGINAL ASYNC HANDLER - HOPE ALL IS WELL DTANK - JOB WELL DONE +async function serveXmltv(response, req) { + try { + const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); + const host = req.headers.host; + const baseUrl = `${protocol}://${host}`; + //const sportsData = await fetchSportsData(); + const formattedContent = fs.readFileSync(EPG_FILE, 'utf-8'); + //const updatedContent = formattedContent + //.replace(/#\[SPORTS\]/g, sportsData || '') + //.replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => { + //return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; + //}); + response.writeHead(200, { + 'Content-Type': 'application/x-mpegURL', + 'Content-Disposition': 'inline; filename="playlist.m3u8"', + }); + response.end(updatedContent); + } catch (error) { + console.error('Error in servePlaylist:', error.message); + response.writeHead(500, { 'Content-Type': 'text/plain' }); + response.end(`Error serving playlist: ${error.message}`); + } +} + +async function servePlaylist(response, req) { + try { + const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); + const host = req.headers.host; + const baseUrl = `${protocol}://${host}`; + //const sportsData = await fetchSportsData(); + const formattedContent = fs.readFileSync(FORMATTED_FILE, 'utf-8'); + const updatedContent = formattedContent + //.replace(/#\[SPORTS\]/g, sportsData || '') + .replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => { + return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; + }) + .replace(/(https?:\/\/[^\s]*tvpass[^\s]*)/g, (fullUrl) => { + return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; + }); + response.writeHead(200, { + 'Content-Type': 'application/x-mpegURL', + 'Content-Disposition': 'inline; filename="playlist.m3u8"', + }); + response.end(updatedContent); + } catch (error) { + console.error('Error in servePlaylist:', error.message); + response.writeHead(500, { 'Content-Type': 'text/plain' }); + response.end(`Error serving playlist: ${error.message}`); + } +} +*/ + +function setCache(key, value, ttl) { + const expiry = Date.now() + ttl; + cache.set(key, { value, expiry }); + log(`Cache set: ${key}, expires in ${ttl / 1000} seconds`); +} + +function getCache(key) { + const cached = cache.get(key); + if (cached && cached.expiry > Date.now()) { + return cached.value; + } else { + if (cached) log(`Cache expired for key: ${key}`); + cache.delete(key); + return null; + } +} + +async function initialize() { + try { + log('Initializing server...'); + await ensureFileExists(externalURL, URLS_FILE); + await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE); + await ensureFileExists(externalEPG, EPG_FILE); + urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean); + if (urls.length === 0) { + throw new Error(`No valid URLs found in ${URLS_FILE}`); + } + log('Initialization complete.'); + } catch (error) { + console.error(`Initialization error: ${error.message}`); + } +} + +const server = http.createServer((req, res) => { + const handleRequest = async () => { + const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); + const host = req.headers.host; + const baseUrl = `${protocol}://${host}`; + if (req.url === '/' && req.method === 'GET') { + const htmlContent = ` + + + + + Playlist Details + + + + +
+
+

Playlist Details

+
+

Playlist URL:

+

EPG URL:

+
+
+
+
+
+ + + +`; + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(htmlContent); + return; + } + if (req.url === '/playlist' && req.method === 'GET') { + log('Playlist request received'); + await servePlaylist(res, req); + return; + } + if (req.url.startsWith('/channel') && req.method === 'GET') { + await serveChannelPlaylist(req, res); + return; + } + if (req.url.startsWith('/key') && req.method === 'GET') { + await serveKey(req, res); + return; + } + if (req.url === '/epg' && req.method === 'GET') { + log('Epg request received'); + await serveXmltv(res, req); + return; + /*res.writeHead(302, { + Location: 'https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml', + }); + res.end(); + return;*/ + } + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + }; + handleRequest().catch((error) => { + console.error('Error handling request:', error); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + }); +}); + +(async () => { + await initialize(); + const PORT = 4124; + server.listen(PORT, '0.0.0.0', () => { + log(`Server is running on port ${PORT}`); + }); +})(); diff --git a/node_modules/.bin/playwright b/node_modules/.bin/playwright new file mode 100644 index 00000000..98e25038 --- /dev/null +++ b/node_modules/.bin/playwright @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../playwright/cli.js" "$@" +else + exec node "$basedir/../playwright/cli.js" "$@" +fi diff --git a/node_modules/.bin/playwright-core b/node_modules/.bin/playwright-core new file mode 100644 index 00000000..bc2c5c8a --- /dev/null +++ b/node_modules/.bin/playwright-core @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../playwright-core/cli.js" "$@" +else + exec node "$basedir/../playwright-core/cli.js" "$@" +fi diff --git a/node_modules/.bin/playwright-core.cmd b/node_modules/.bin/playwright-core.cmd new file mode 100644 index 00000000..11282048 --- /dev/null +++ b/node_modules/.bin/playwright-core.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\playwright-core\cli.js" %* diff --git a/node_modules/.bin/playwright-core.ps1 b/node_modules/.bin/playwright-core.ps1 new file mode 100644 index 00000000..e914b999 --- /dev/null +++ b/node_modules/.bin/playwright-core.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../playwright-core/cli.js" $args + } else { + & "$basedir/node$exe" "$basedir/../playwright-core/cli.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../playwright-core/cli.js" $args + } else { + & "node$exe" "$basedir/../playwright-core/cli.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/node_modules/.bin/playwright.cmd b/node_modules/.bin/playwright.cmd new file mode 100644 index 00000000..88713a45 --- /dev/null +++ b/node_modules/.bin/playwright.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\playwright\cli.js" %* diff --git a/node_modules/.bin/playwright.ps1 b/node_modules/.bin/playwright.ps1 new file mode 100644 index 00000000..efa8f924 --- /dev/null +++ b/node_modules/.bin/playwright.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../playwright/cli.js" $args + } else { + & "$basedir/node$exe" "$basedir/../playwright/cli.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../playwright/cli.js" $args + } else { + & "node$exe" "$basedir/../playwright/cli.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 00000000..3f314246 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "thetvapp-m3u", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/user-agents": { + "version": "1.1.388", + "resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.1.388.tgz", + "integrity": "sha512-zsFa+jzuM+7DiB9es9iYL0fbPeN0Kc9Bor6PcNwKN1X46yHus3oXrH3RTyJ1CsVxQpzG9iXdU3V3Io0FboFvhQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0" + } + } + } +} diff --git a/node_modules/lodash.clonedeep/LICENSE b/node_modules/lodash.clonedeep/LICENSE new file mode 100644 index 00000000..e0c69d56 --- /dev/null +++ b/node_modules/lodash.clonedeep/LICENSE @@ -0,0 +1,47 @@ +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. diff --git a/node_modules/lodash.clonedeep/README.md b/node_modules/lodash.clonedeep/README.md new file mode 100644 index 00000000..fee48e47 --- /dev/null +++ b/node_modules/lodash.clonedeep/README.md @@ -0,0 +1,18 @@ +# lodash.clonedeep v4.5.0 + +The [lodash](https://lodash.com/) method `_.cloneDeep` exported as a [Node.js](https://nodejs.org/) module. + +## Installation + +Using npm: +```bash +$ {sudo -H} npm i -g npm +$ npm i --save lodash.clonedeep +``` + +In Node.js: +```js +var cloneDeep = require('lodash.clonedeep'); +``` + +See the [documentation](https://lodash.com/docs#cloneDeep) or [package source](https://github.com/lodash/lodash/blob/4.5.0-npm-packages/lodash.clonedeep) for more details. diff --git a/node_modules/lodash.clonedeep/index.js b/node_modules/lodash.clonedeep/index.js new file mode 100644 index 00000000..1b0e5029 --- /dev/null +++ b/node_modules/lodash.clonedeep/index.js @@ -0,0 +1,1748 @@ +/** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + +/** Used as the size to enable large array optimizations. */ +var LARGE_ARRAY_SIZE = 200; + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** Used as references for various `Number` constants. */ +var MAX_SAFE_INTEGER = 9007199254740991; + +/** `Object#toString` result references. */ +var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + symbolTag = '[object Symbol]', + weakMapTag = '[object WeakMap]'; + +var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + +/** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + +/** Used to match `RegExp` flags from their coerced string values. */ +var reFlags = /\w*$/; + +/** Used to detect host constructors (Safari). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used to detect unsigned integer values. */ +var reIsUint = /^(?:0|[1-9]\d*)$/; + +/** Used to identify `toStringTag` values supported by `_.clone`. */ +var cloneableTags = {}; +cloneableTags[argsTag] = cloneableTags[arrayTag] = +cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = +cloneableTags[boolTag] = cloneableTags[dateTag] = +cloneableTags[float32Tag] = cloneableTags[float64Tag] = +cloneableTags[int8Tag] = cloneableTags[int16Tag] = +cloneableTags[int32Tag] = cloneableTags[mapTag] = +cloneableTags[numberTag] = cloneableTags[objectTag] = +cloneableTags[regexpTag] = cloneableTags[setTag] = +cloneableTags[stringTag] = cloneableTags[symbolTag] = +cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = +cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; +cloneableTags[errorTag] = cloneableTags[funcTag] = +cloneableTags[weakMapTag] = false; + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +/** Detect free variable `exports`. */ +var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; + +/** Detect free variable `module`. */ +var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; + +/** Detect the popular CommonJS extension `module.exports`. */ +var moduleExports = freeModule && freeModule.exports === freeExports; + +/** + * Adds the key-value `pair` to `map`. + * + * @private + * @param {Object} map The map to modify. + * @param {Array} pair The key-value pair to add. + * @returns {Object} Returns `map`. + */ +function addMapEntry(map, pair) { + // Don't return `map.set` because it's not chainable in IE 11. + map.set(pair[0], pair[1]); + return map; +} + +/** + * Adds `value` to `set`. + * + * @private + * @param {Object} set The set to modify. + * @param {*} value The value to add. + * @returns {Object} Returns `set`. + */ +function addSetEntry(set, value) { + // Don't return `set.add` because it's not chainable in IE 11. + set.add(value); + return set; +} + +/** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ +function arrayEach(array, iteratee) { + var index = -1, + length = array ? array.length : 0; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; +} + +/** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ +function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; +} + +/** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ +function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array ? array.length : 0; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; +} + +/** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ +function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; +} + +/** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function getValue(object, key) { + return object == null ? undefined : object[key]; +} + +/** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ +function isHostObject(value) { + // Many host objects are `Object` objects that can coerce to strings + // despite having improperly defined `toString` methods. + var result = false; + if (value != null && typeof value.toString != 'function') { + try { + result = !!(value + ''); + } catch (e) {} + } + return result; +} + +/** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ +function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; +} + +/** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ +function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; +} + +/** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ +function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; +} + +/** Used for built-in method references. */ +var arrayProto = Array.prototype, + funcProto = Function.prototype, + objectProto = Object.prototype; + +/** Used to detect overreaching core-js shims. */ +var coreJsData = root['__core-js_shared__']; + +/** Used to detect methods masquerading as native. */ +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var objectToString = objectProto.toString; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** Built-in value references. */ +var Buffer = moduleExports ? root.Buffer : undefined, + Symbol = root.Symbol, + Uint8Array = root.Uint8Array, + getPrototype = overArg(Object.getPrototypeOf, Object), + objectCreate = Object.create, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeGetSymbols = Object.getOwnPropertySymbols, + nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, + nativeKeys = overArg(Object.keys, Object); + +/* Built-in method references that are verified to be native. */ +var DataView = getNative(root, 'DataView'), + Map = getNative(root, 'Map'), + Promise = getNative(root, 'Promise'), + Set = getNative(root, 'Set'), + WeakMap = getNative(root, 'WeakMap'), + nativeCreate = getNative(Object, 'create'); + +/** Used to detect maps, sets, and weakmaps. */ +var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map), + promiseCtorString = toSource(Promise), + setCtorString = toSource(Set), + weakMapCtorString = toSource(WeakMap); + +/** Used to convert symbols to primitives and strings. */ +var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto ? symbolProto.valueOf : undefined; + +/** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Hash(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +/** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; +} + +/** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function hashDelete(key) { + return this.has(key) && delete this.__data__[key]; +} + +/** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; +} + +/** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key); +} + +/** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ +function hashSet(key, value) { + var data = this.__data__; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; +} + +// Add methods to `Hash`. +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; + +/** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function ListCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +/** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ +function listCacheClear() { + this.__data__ = []; +} + +/** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + return true; +} + +/** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} + +/** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} + +/** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} + +// Add methods to `ListCache`. +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; + +/** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function MapCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +/** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ +function mapCacheClear() { + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; +} + +/** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function mapCacheDelete(key) { + return getMapData(this, key)['delete'](key); +} + +/** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} + +/** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} + +/** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ +function mapCacheSet(key, value) { + getMapData(this, key).set(key, value); + return this; +} + +// Add methods to `MapCache`. +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; + +/** + * Creates a stack cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Stack(entries) { + this.__data__ = new ListCache(entries); +} + +/** + * Removes all key-value entries from the stack. + * + * @private + * @name clear + * @memberOf Stack + */ +function stackClear() { + this.__data__ = new ListCache; +} + +/** + * Removes `key` and its value from the stack. + * + * @private + * @name delete + * @memberOf Stack + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function stackDelete(key) { + return this.__data__['delete'](key); +} + +/** + * Gets the stack value for `key`. + * + * @private + * @name get + * @memberOf Stack + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function stackGet(key) { + return this.__data__.get(key); +} + +/** + * Checks if a stack value for `key` exists. + * + * @private + * @name has + * @memberOf Stack + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function stackHas(key) { + return this.__data__.has(key); +} + +/** + * Sets the stack `key` to `value`. + * + * @private + * @name set + * @memberOf Stack + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the stack cache instance. + */ +function stackSet(key, value) { + var cache = this.__data__; + if (cache instanceof ListCache) { + var pairs = cache.__data__; + if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + return this; + } + cache = this.__data__ = new MapCache(pairs); + } + cache.set(key, value); + return this; +} + +// Add methods to `Stack`. +Stack.prototype.clear = stackClear; +Stack.prototype['delete'] = stackDelete; +Stack.prototype.get = stackGet; +Stack.prototype.has = stackHas; +Stack.prototype.set = stackSet; + +/** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ +function arrayLikeKeys(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray(value) || isArguments(value)) + ? baseTimes(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex(key, length)))) { + result.push(key); + } + } + return result; +} + +/** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ +function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + object[key] = value; + } +} + +/** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} + +/** + * The base implementation of `_.assign` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ +function baseAssign(object, source) { + return object && copyObject(source, keys(source), object); +} + +/** + * The base implementation of `_.clone` and `_.cloneDeep` which tracks + * traversed objects. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @param {boolean} [isFull] Specify a clone including symbols. + * @param {Function} [customizer] The function to customize cloning. + * @param {string} [key] The key of `value`. + * @param {Object} [object] The parent object of `value`. + * @param {Object} [stack] Tracks traversed objects and their clone counterparts. + * @returns {*} Returns the cloned value. + */ +function baseClone(value, isDeep, isFull, customizer, key, object, stack) { + var result; + if (customizer) { + result = object ? customizer(value, key, object, stack) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return copyArray(value, result); + } + } else { + var tag = getTag(value), + isFunc = tag == funcTag || tag == genTag; + + if (isBuffer(value)) { + return cloneBuffer(value, isDeep); + } + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + if (isHostObject(value)) { + return object ? value : {}; + } + result = initCloneObject(isFunc ? {} : value); + if (!isDeep) { + return copySymbols(value, baseAssign(result, value)); + } + } else { + if (!cloneableTags[tag]) { + return object ? value : {}; + } + result = initCloneByTag(value, tag, baseClone, isDeep); + } + } + // Check for circular references and return its corresponding clone. + stack || (stack = new Stack); + var stacked = stack.get(value); + if (stacked) { + return stacked; + } + stack.set(value, result); + + if (!isArr) { + var props = isFull ? getAllKeys(value) : keys(value); + } + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } + // Recursively populate clone (susceptible to call stack limits). + assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack)); + }); + return result; +} + +/** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ +function baseCreate(proto) { + return isObject(proto) ? objectCreate(proto) : {}; +} + +/** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ +function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); +} + +/** + * The base implementation of `getTag`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + return objectToString.call(value); +} + +/** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} + +/** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ +function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; +} + +/** + * Creates a clone of `buffer`. + * + * @private + * @param {Buffer} buffer The buffer to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Buffer} Returns the cloned buffer. + */ +function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var result = new buffer.constructor(buffer.length); + buffer.copy(result); + return result; +} + +/** + * Creates a clone of `arrayBuffer`. + * + * @private + * @param {ArrayBuffer} arrayBuffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ +function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; +} + +/** + * Creates a clone of `dataView`. + * + * @private + * @param {Object} dataView The data view to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned data view. + */ +function cloneDataView(dataView, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; + return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); +} + +/** + * Creates a clone of `map`. + * + * @private + * @param {Object} map The map to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned map. + */ +function cloneMap(map, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map); + return arrayReduce(array, addMapEntry, new map.constructor); +} + +/** + * Creates a clone of `regexp`. + * + * @private + * @param {Object} regexp The regexp to clone. + * @returns {Object} Returns the cloned regexp. + */ +function cloneRegExp(regexp) { + var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); + result.lastIndex = regexp.lastIndex; + return result; +} + +/** + * Creates a clone of `set`. + * + * @private + * @param {Object} set The set to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned set. + */ +function cloneSet(set, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set); + return arrayReduce(array, addSetEntry, new set.constructor); +} + +/** + * Creates a clone of the `symbol` object. + * + * @private + * @param {Object} symbol The symbol object to clone. + * @returns {Object} Returns the cloned symbol object. + */ +function cloneSymbol(symbol) { + return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; +} + +/** + * Creates a clone of `typedArray`. + * + * @private + * @param {Object} typedArray The typed array to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned typed array. + */ +function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); +} + +/** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ +function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; +} + +/** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property identifiers to copy. + * @param {Object} [object={}] The object to copy properties to. + * @param {Function} [customizer] The function to customize copied values. + * @returns {Object} Returns `object`. + */ +function copyObject(source, props, object, customizer) { + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + assignValue(object, key, newValue === undefined ? source[key] : newValue); + } + return object; +} + +/** + * Copies own symbol properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ +function copySymbols(source, object) { + return copyObject(source, getSymbols(source), object); +} + +/** + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ +function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); +} + +/** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} + +/** + * Creates an array of the own enumerable symbol properties of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ +var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray; + +/** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +var getTag = baseGetTag; + +// Fallback for data views, maps, sets, and weak maps in IE 11, +// for data views in Edge < 14, and promises in Node.js. +if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map && getTag(new Map) != mapTag) || + (Promise && getTag(Promise.resolve()) != promiseTag) || + (Set && getTag(new Set) != setTag) || + (WeakMap && getTag(new WeakMap) != weakMapTag)) { + getTag = function(value) { + var result = objectToString.call(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : undefined; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; +} + +/** + * Initializes an array clone. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the initialized clone. + */ +function initCloneArray(array) { + var length = array.length, + result = array.constructor(length); + + // Add properties assigned by `RegExp#exec`. + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; +} + +/** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ +function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; +} + +/** + * Initializes an object clone based on its `toStringTag`. + * + * **Note:** This function only supports cloning values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to clone. + * @param {string} tag The `toStringTag` of the object to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the initialized clone. + */ +function initCloneByTag(object, tag, cloneFunc, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return cloneArrayBuffer(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case dataViewTag: + return cloneDataView(object, isDeep); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + return cloneTypedArray(object, isDeep); + + case mapTag: + return cloneMap(object, isDeep, cloneFunc); + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + return cloneRegExp(object); + + case setTag: + return cloneSet(object, isDeep, cloneFunc); + + case symbolTag: + return cloneSymbol(object); + } +} + +/** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ +function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); +} + +/** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} + +/** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} + +/** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ +function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; + + return value === proto; +} + +/** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to process. + * @returns {string} Returns the source code. + */ +function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} + +/** + * This method is like `_.clone` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @returns {*} Returns the deep cloned value. + * @see _.clone + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var deep = _.cloneDeep(objects); + * console.log(deep[0] === objects[0]); + * // => false + */ +function cloneDeep(value) { + return baseClone(value, true, true); +} + +/** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ +function eq(value, other) { + return value === other || (value !== value && other !== other); +} + +/** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ +function isArguments(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') && + (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag); +} + +/** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ +var isArray = Array.isArray; + +/** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ +function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); +} + +/** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ +function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); +} + +/** + * Checks if `value` is a buffer. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. + * @example + * + * _.isBuffer(new Buffer(2)); + * // => true + * + * _.isBuffer(new Uint8Array(2)); + * // => false + */ +var isBuffer = nativeIsBuffer || stubFalse; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject(value) ? objectToString.call(value) : ''; + return tag == funcTag || tag == genTag; +} + +/** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ +function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; +} + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); +} + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return !!value && typeof value == 'object'; +} + +/** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ +function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); +} + +/** + * This method returns a new empty array. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {Array} Returns the new empty array. + * @example + * + * var arrays = _.times(2, _.stubArray); + * + * console.log(arrays); + * // => [[], []] + * + * console.log(arrays[0] === arrays[1]); + * // => false + */ +function stubArray() { + return []; +} + +/** + * This method returns `false`. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {boolean} Returns `false`. + * @example + * + * _.times(2, _.stubFalse); + * // => [false, false] + */ +function stubFalse() { + return false; +} + +module.exports = cloneDeep; diff --git a/node_modules/lodash.clonedeep/package.json b/node_modules/lodash.clonedeep/package.json new file mode 100644 index 00000000..fb1d626d --- /dev/null +++ b/node_modules/lodash.clonedeep/package.json @@ -0,0 +1,17 @@ +{ + "name": "lodash.clonedeep", + "version": "4.5.0", + "description": "The lodash method `_.cloneDeep` exported as a module.", + "homepage": "https://lodash.com/", + "icon": "https://lodash.com/icon.svg", + "license": "MIT", + "keywords": "lodash-modularized, clonedeep", + "author": "John-David Dalton (http://allyoucanleet.com/)", + "contributors": [ + "John-David Dalton (http://allyoucanleet.com/)", + "Blaine Bublitz (https://github.com/phated)", + "Mathias Bynens (https://mathiasbynens.be/)" + ], + "repository": "lodash/lodash", + "scripts": { "test": "echo \"See https://travis-ci.org/lodash/lodash-cli for testing details.\"" } +} diff --git a/node_modules/playwright-core/LICENSE b/node_modules/playwright-core/LICENSE new file mode 100644 index 00000000..4ace03dd --- /dev/null +++ b/node_modules/playwright-core/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Portions Copyright (c) Microsoft Corporation. + Portions Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/node_modules/playwright-core/NOTICE b/node_modules/playwright-core/NOTICE new file mode 100644 index 00000000..814ec169 --- /dev/null +++ b/node_modules/playwright-core/NOTICE @@ -0,0 +1,5 @@ +Playwright +Copyright (c) Microsoft Corporation + +This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer), +available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE). diff --git a/node_modules/playwright-core/README.md b/node_modules/playwright-core/README.md new file mode 100644 index 00000000..422b3739 --- /dev/null +++ b/node_modules/playwright-core/README.md @@ -0,0 +1,3 @@ +# playwright-core + +This package contains the no-browser flavor of [Playwright](http://github.com/microsoft/playwright). diff --git a/node_modules/playwright-core/bin/README.md b/node_modules/playwright-core/bin/README.md new file mode 100644 index 00000000..2426643d --- /dev/null +++ b/node_modules/playwright-core/bin/README.md @@ -0,0 +1,2 @@ +See building instructions at [`/browser_patches/winldd/README.md`](../../../browser_patches/winldd/README.md) + diff --git a/node_modules/playwright-core/bin/install_media_pack.ps1 b/node_modules/playwright-core/bin/install_media_pack.ps1 new file mode 100644 index 00000000..61707542 --- /dev/null +++ b/node_modules/playwright-core/bin/install_media_pack.ps1 @@ -0,0 +1,5 @@ +$osInfo = Get-WmiObject -Class Win32_OperatingSystem +# check if running on Windows Server +if ($osInfo.ProductType -eq 3) { + Install-WindowsFeature Server-Media-Foundation +} diff --git a/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh b/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh new file mode 100644 index 00000000..0451bda3 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old beta if any. +if dpkg --get-selections | grep -q "^google-chrome-beta[[:space:]]*install$" >/dev/null; then + apt-get remove -y google-chrome-beta +fi + +# 2. Update apt lists (needed to install curl and chrome dependencies) +apt-get update + +# 3. Install curl to download chrome +if ! command -v curl >/dev/null; then + apt-get install -y curl +fi + +# 4. download chrome beta from dl.google.com and install it. +cd /tmp +curl -O https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb +apt-get install -y ./google-chrome-beta_current_amd64.deb +rm -rf ./google-chrome-beta_current_amd64.deb +cd - +google-chrome-beta --version diff --git a/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh b/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh new file mode 100644 index 00000000..b6e1990c --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e +set -x + +rm -rf "/Applications/Google Chrome Beta.app" +cd /tmp +curl -o ./googlechromebeta.dmg -k https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg +hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg +cp -pR "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications +hdiutil detach /Volumes/googlechromebeta.dmg +rm -rf /tmp/googlechromebeta.dmg + +/Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta --version diff --git a/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 b/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 new file mode 100644 index 00000000..3fbe5515 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' + +$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromebetastandaloneenterprise64.msi' + +Write-Host "Downloading Google Chrome Beta" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\google-chrome-beta.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Google Chrome Beta" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Google\\Chrome Beta\\Application\\chrome.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Google Chrome Beta." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh b/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh new file mode 100644 index 00000000..78f1d413 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old stable if any. +if dpkg --get-selections | grep -q "^google-chrome[[:space:]]*install$" >/dev/null; then + apt-get remove -y google-chrome +fi + +# 2. Update apt lists (needed to install curl and chrome dependencies) +apt-get update + +# 3. Install curl to download chrome +if ! command -v curl >/dev/null; then + apt-get install -y curl +fi + +# 4. download chrome stable from dl.google.com and install it. +cd /tmp +curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +apt-get install -y ./google-chrome-stable_current_amd64.deb +rm -rf ./google-chrome-stable_current_amd64.deb +cd - +google-chrome --version diff --git a/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh b/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh new file mode 100644 index 00000000..91d826c0 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -e +set -x + +rm -rf "/Applications/Google Chrome.app" +cd /tmp +curl -o ./googlechrome.dmg -k https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg +hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg +cp -pR "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications +hdiutil detach /Volumes/googlechrome.dmg +rm -rf /tmp/googlechrome.dmg +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version diff --git a/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 b/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 new file mode 100644 index 00000000..7ca2dbaf --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' +$url = 'https://dl.google.com/tag/s/dl/chrome/install/googlechromestandaloneenterprise64.msi' + +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\google-chrome.msi" +Write-Host "Downloading Google Chrome" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Google Chrome" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + + +$suffix = "\\Google\\Chrome\\Application\\chrome.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Google Chrome." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh b/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh new file mode 100644 index 00000000..ececd05a --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old beta if any. +if dpkg --get-selections | grep -q "^microsoft-edge-beta[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-beta +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-beta + +microsoft-edge-beta --version diff --git a/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh b/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh new file mode 100644 index 00000000..69c06024 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl -o ./msedge_beta.pkg -k "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_beta.pkg -target / +rm -rf /tmp/msedge_beta.pkg +/Applications/Microsoft\ Edge\ Beta.app/Contents/MacOS/Microsoft\ Edge\ Beta --version diff --git a/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 b/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 new file mode 100644 index 00000000..cce0d0bf --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = 'Stop' +$url = $args[0] + +Write-Host "Downloading Microsoft Edge Beta" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-beta.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge Beta" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge Beta\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge Beta." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh b/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh new file mode 100644 index 00000000..6ab84c31 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old dev if any. +if dpkg --get-selections | grep -q "^microsoft-edge-dev[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-dev +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-dev + +microsoft-edge-dev --version diff --git a/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh b/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh new file mode 100644 index 00000000..0ad05b0a --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl -o ./msedge_dev.pkg -k "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_dev.pkg -target / +rm -rf /tmp/msedge_dev.pkg +/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev --version diff --git a/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 b/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 new file mode 100644 index 00000000..22e6db84 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = 'Stop' +$url = $args[0] + +Write-Host "Downloading Microsoft Edge Dev" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-dev.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge Dev" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge Dev\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge Dev." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh b/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh new file mode 100644 index 00000000..e66f85bb --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old stable if any. +if dpkg --get-selections | grep -q "^microsoft-edge-stable[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-stable +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-stable.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-stable + +microsoft-edge-stable --version diff --git a/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh b/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh new file mode 100644 index 00000000..b82cfb37 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl -o ./msedge_stable.pkg -k "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_stable.pkg -target / +rm -rf /tmp/msedge_stable.pkg +/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --version diff --git a/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 b/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 new file mode 100644 index 00000000..31fdf513 --- /dev/null +++ b/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' + +$url = $args[0] + +Write-Host "Downloading Microsoft Edge" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-stable.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} \ No newline at end of file diff --git a/node_modules/playwright-core/browsers.json b/node_modules/playwright-core/browsers.json new file mode 100644 index 00000000..81f1d3af --- /dev/null +++ b/node_modules/playwright-core/browsers.json @@ -0,0 +1,65 @@ +{ + "comment": "Do not edit this file, use utils/roll_browser.js", + "browsers": [ + { + "name": "chromium", + "revision": "1148", + "installByDefault": true, + "browserVersion": "131.0.6778.33" + }, + { + "name": "chromium-headless-shell", + "revision": "1148", + "installByDefault": true, + "browserVersion": "131.0.6778.33" + }, + { + "name": "chromium-tip-of-tree", + "revision": "1277", + "installByDefault": false, + "browserVersion": "132.0.6834.0" + }, + { + "name": "firefox", + "revision": "1466", + "installByDefault": true, + "browserVersion": "132.0" + }, + { + "name": "firefox-beta", + "revision": "1465", + "installByDefault": false, + "browserVersion": "132.0b8" + }, + { + "name": "webkit", + "revision": "2104", + "installByDefault": true, + "revisionOverrides": { + "mac10.14": "1446", + "mac10.15": "1616", + "mac11": "1816", + "mac11-arm64": "1816", + "mac12": "2009", + "mac12-arm64": "2009", + "ubuntu20.04-x64": "2092", + "ubuntu20.04-arm64": "2092" + }, + "browserVersion": "18.2" + }, + { + "name": "ffmpeg", + "revision": "1010", + "installByDefault": true, + "revisionOverrides": { + "mac12": "1010", + "mac12-arm64": "1010" + } + }, + { + "name": "android", + "revision": "1001", + "installByDefault": false + } + ] +} diff --git a/node_modules/playwright-core/cli.js b/node_modules/playwright-core/cli.js new file mode 100644 index 00000000..fb309ead --- /dev/null +++ b/node_modules/playwright-core/cli.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const { program } = require('./lib/cli/programWithTestStub'); +program.parse(process.argv); diff --git a/node_modules/playwright-core/index.d.ts b/node_modules/playwright-core/index.d.ts new file mode 100644 index 00000000..97c14936 --- /dev/null +++ b/node_modules/playwright-core/index.d.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types/types'; diff --git a/node_modules/playwright-core/index.js b/node_modules/playwright-core/index.js new file mode 100644 index 00000000..3d246e9f --- /dev/null +++ b/node_modules/playwright-core/index.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const minimumMajorNodeVersion = 14; +const currentNodeVersion = process.versions.node; +const semver = currentNodeVersion.split('.'); +const [major] = [+semver[0]]; + +if (major < minimumMajorNodeVersion) { + // eslint-disable-next-line no-console + console.error( + 'You are running Node.js ' + + currentNodeVersion + + '.\n' + + `Playwright requires Node.js ${minimumMajorNodeVersion} or higher. \n` + + 'Please update your version of Node.js.' + ); + process.exit(1); +} + +module.exports = require('./lib/inprocess'); diff --git a/node_modules/playwright-core/index.mjs b/node_modules/playwright-core/index.mjs new file mode 100644 index 00000000..3b3c75b0 --- /dev/null +++ b/node_modules/playwright-core/index.mjs @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import playwright from './index.js'; + +export const chromium = playwright.chromium; +export const firefox = playwright.firefox; +export const webkit = playwright.webkit; +export const selectors = playwright.selectors; +export const devices = playwright.devices; +export const errors = playwright.errors; +export const request = playwright.request; +export const _electron = playwright._electron; +export const _android = playwright._android; +export default playwright; diff --git a/node_modules/playwright-core/lib/androidServerImpl.js b/node_modules/playwright-core/lib/androidServerImpl.js new file mode 100644 index 00000000..72556cb6 --- /dev/null +++ b/node_modules/playwright-core/lib/androidServerImpl.js @@ -0,0 +1,69 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AndroidServerLauncherImpl = void 0; +var _utilsBundle = require("./utilsBundle"); +var _utils = require("./utils"); +var _playwright = require("./server/playwright"); +var _playwrightServer = require("./remote/playwrightServer"); +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class AndroidServerLauncherImpl { + async launchServer(options = {}) { + const playwright = (0, _playwright.createPlaywright)({ + sdkLanguage: 'javascript', + isServer: true + }); + // 1. Pre-connect to the device + let devices = await playwright.android.devices({ + host: options.adbHost, + port: options.adbPort, + omitDriverInstall: options.omitDriverInstall + }); + if (devices.length === 0) throw new Error('No devices found'); + if (options.deviceSerialNumber) { + devices = devices.filter(d => d.serial === options.deviceSerialNumber); + if (devices.length === 0) throw new Error(`No device with serial number '${options.deviceSerialNumber}' not found`); + } + if (devices.length > 1) throw new Error(`More than one device found. Please specify deviceSerialNumber`); + const device = devices[0]; + const path = options.wsPath ? options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}` : `/${(0, _utils.createGuid)()}`; + + // 2. Start the server + const server = new _playwrightServer.PlaywrightServer({ + mode: 'launchServer', + path, + maxConnections: 1, + preLaunchedAndroidDevice: device + }); + const wsEndpoint = await server.listen(options.port, options.host); + + // 3. Return the BrowserServer interface + const browserServer = new _utilsBundle.ws.EventEmitter(); + browserServer.wsEndpoint = () => wsEndpoint; + browserServer.close = () => device.close(); + browserServer.kill = () => device.close(); + device.on('close', () => { + server.close(); + browserServer.emit('close'); + }); + return browserServer; + } +} +exports.AndroidServerLauncherImpl = AndroidServerLauncherImpl; \ No newline at end of file diff --git a/node_modules/playwright-core/lib/browserServerImpl.js b/node_modules/playwright-core/lib/browserServerImpl.js new file mode 100644 index 00000000..f6101fe3 --- /dev/null +++ b/node_modules/playwright-core/lib/browserServerImpl.js @@ -0,0 +1,92 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.BrowserServerLauncherImpl = void 0; +var _utilsBundle = require("./utilsBundle"); +var _clientHelper = require("./client/clientHelper"); +var _utils = require("./utils"); +var _instrumentation = require("./server/instrumentation"); +var _playwright = require("./server/playwright"); +var _playwrightServer = require("./remote/playwrightServer"); +var _helper = require("./server/helper"); +var _stackTrace = require("./utils/stackTrace"); +var _socksProxy = require("./common/socksProxy"); +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class BrowserServerLauncherImpl { + constructor(browserName) { + this._browserName = void 0; + this._browserName = browserName; + } + async launchServer(options = {}) { + const playwright = (0, _playwright.createPlaywright)({ + sdkLanguage: 'javascript', + isServer: true + }); + // TODO: enable socks proxy once ipv6 is supported. + const socksProxy = false ? new _socksProxy.SocksProxy() : undefined; + playwright.options.socksProxyPort = await (socksProxy === null || socksProxy === void 0 ? void 0 : socksProxy.listen(0)); + + // 1. Pre-launch the browser + const metadata = (0, _instrumentation.serverSideCallMetadata)(); + const browser = await playwright[this._browserName].launch(metadata, { + ...options, + ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, + ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), + env: options.env ? (0, _clientHelper.envObjectToArray)(options.env) : undefined + }, toProtocolLogger(options.logger)).catch(e => { + const log = _helper.helper.formatBrowserLogs(metadata.log); + (0, _stackTrace.rewriteErrorMessage)(e, `${e.message} Failed to launch browser.${log}`); + throw e; + }); + const path = options.wsPath ? options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}` : `/${(0, _utils.createGuid)()}`; + + // 2. Start the server + const server = new _playwrightServer.PlaywrightServer({ + mode: 'launchServer', + path, + maxConnections: Infinity, + preLaunchedBrowser: browser, + preLaunchedSocksProxy: socksProxy + }); + const wsEndpoint = await server.listen(options.port, options.host); + + // 3. Return the BrowserServer interface + const browserServer = new _utilsBundle.ws.EventEmitter(); + browserServer.process = () => browser.options.browserProcess.process; + browserServer.wsEndpoint = () => wsEndpoint; + browserServer.close = () => browser.options.browserProcess.close(); + browserServer[Symbol.asyncDispose] = browserServer.close; + browserServer.kill = () => browser.options.browserProcess.kill(); + browserServer._disconnectForTest = () => server.close(); + browserServer._userDataDirForTest = browser._userDataDirForTest; + browser.options.browserProcess.onclose = (exitCode, signal) => { + socksProxy === null || socksProxy === void 0 || socksProxy.close().catch(() => {}); + server.close(); + browserServer.emit('close', exitCode, signal); + }; + return browserServer; + } +} +exports.BrowserServerLauncherImpl = BrowserServerLauncherImpl; +function toProtocolLogger(logger) { + return logger ? (direction, message) => { + if (logger.isEnabled('protocol', 'verbose')) logger.log('protocol', 'verbose', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message), [], {}); + } : undefined; +} \ No newline at end of file diff --git a/node_modules/playwright-core/lib/cli/driver.js b/node_modules/playwright-core/lib/cli/driver.js new file mode 100644 index 00000000..ddbf3e5d --- /dev/null +++ b/node_modules/playwright-core/lib/cli/driver.js @@ -0,0 +1,95 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.launchBrowserServer = launchBrowserServer; +exports.printApiJson = printApiJson; +exports.runDriver = runDriver; +exports.runServer = runServer; +var _fs = _interopRequireDefault(require("fs")); +var playwright = _interopRequireWildcard(require("../..")); +var _server = require("../server"); +var _transport = require("../protocol/transport"); +var _playwrightServer = require("../remote/playwrightServer"); +var _processLauncher = require("../utils/processLauncher"); +function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } +function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable no-console */ + +function printApiJson() { + // Note: this file is generated by build-playwright-driver.sh + console.log(JSON.stringify(require('../../api.json'))); +} +function runDriver() { + const dispatcherConnection = new _server.DispatcherConnection(); + new _server.RootDispatcher(dispatcherConnection, async (rootScope, { + sdkLanguage + }) => { + const playwright = (0, _server.createPlaywright)({ + sdkLanguage + }); + return new _server.PlaywrightDispatcher(rootScope, playwright); + }); + const transport = new _transport.PipeTransport(process.stdout, process.stdin); + transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message)); + // Certain Language Binding JSON parsers (e.g. .NET) do not like strings with lone surrogates. + const isJavaScriptLanguageBinding = !process.env.PW_LANG_NAME || process.env.PW_LANG_NAME === 'javascript'; + const replacer = !isJavaScriptLanguageBinding && String.prototype.toWellFormed ? (key, value) => { + if (typeof value === 'string') return value.toWellFormed(); + return value; + } : undefined; + dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message, replacer)); + transport.onclose = () => { + // Drop any messages during shutdown on the floor. + dispatcherConnection.onmessage = () => {}; + (0, _processLauncher.gracefullyProcessExitDoNotHang)(0); + }; + // Ignore the SIGINT signal in the driver process so the parent can gracefully close the connection. + // We still will destruct everything (close browsers and exit) when the transport pipe closes. + process.on('SIGINT', () => { + // Keep the process running. + }); +} +async function runServer(options) { + const { + port, + host, + path = '/', + maxConnections = Infinity, + extension + } = options; + const server = new _playwrightServer.PlaywrightServer({ + mode: extension ? 'extension' : 'default', + path, + maxConnections + }); + const wsEndpoint = await server.listen(port, host); + process.on('exit', () => server.close().catch(console.error)); + console.log('Listening on ' + wsEndpoint); + process.stdin.on('close', () => (0, _processLauncher.gracefullyProcessExitDoNotHang)(0)); +} +async function launchBrowserServer(browserName, configFile) { + let options = {}; + if (configFile) options = JSON.parse(_fs.default.readFileSync(configFile).toString()); + const browserType = playwright[browserName]; + const server = await browserType.launchServer(options); + console.log(server.wsEndpoint()); +} \ No newline at end of file diff --git a/node_modules/playwright-core/lib/cli/program.js b/node_modules/playwright-core/lib/cli/program.js new file mode 100644 index 00000000..ce7bd998 --- /dev/null +++ b/node_modules/playwright-core/lib/cli/program.js @@ -0,0 +1,607 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "program", { + enumerable: true, + get: function () { + return _utilsBundle.program; + } +}); +var _fs = _interopRequireDefault(require("fs")); +var _os = _interopRequireDefault(require("os")); +var _path = _interopRequireDefault(require("path")); +var _utilsBundle = require("../utilsBundle"); +var _driver = require("./driver"); +var _traceViewer = require("../server/trace/viewer/traceViewer"); +var playwright = _interopRequireWildcard(require("../..")); +var _child_process = require("child_process"); +var _utils = require("../utils"); +var _server = require("../server"); +var _errors = require("../client/errors"); +function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } +function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable no-console */ + +const packageJSON = require('../../package.json'); +_utilsBundle.program.version('Version ' + (process.env.PW_CLI_DISPLAY_VERSION || packageJSON.version)).name(buildBasePlaywrightCLICommand(process.env.PW_LANG_NAME)); +_utilsBundle.program.command('mark-docker-image [dockerImageNameTemplate]', { + hidden: true +}).description('mark docker image').allowUnknownOption(true).action(function (dockerImageNameTemplate) { + (0, _utils.assert)(dockerImageNameTemplate, 'dockerImageNameTemplate is required'); + (0, _server.writeDockerVersion)(dockerImageNameTemplate).catch(logErrorAndExit); +}); +commandWithOpenOptions('open [url]', 'open page in browser specified via -b, --browser', []).action(function (url, options) { + open(options, url, codegenId()).catch(logErrorAndExit); +}).addHelpText('afterAll', ` +Examples: + + $ open + $ open -b webkit https://example.com`); +commandWithOpenOptions('codegen [url]', 'open page and generate code for user actions', [['-o, --output ', 'saves the generated script to a file'], ['--target ', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()], ['--save-trace ', 'record a trace for the session and save it to a file'], ['--test-id-attribute ', 'use the specified attribute to generate data test ID selectors']]).action(function (url, options) { + codegen(options, url).catch(logErrorAndExit); +}).addHelpText('afterAll', ` +Examples: + + $ codegen + $ codegen --target=python + $ codegen -b webkit https://example.com`); +_utilsBundle.program.command('debug [args...]', { + hidden: true +}).description('run command in debug mode: disable timeout, open inspector').allowUnknownOption(true).action(function (app, options) { + (0, _child_process.spawn)(app, options, { + env: { + ...process.env, + PWDEBUG: '1' + }, + stdio: 'inherit' + }); +}).addHelpText('afterAll', ` +Examples: + + $ debug node test.js + $ debug npm run test`); +function suggestedBrowsersToInstall() { + return _server.registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', '); +} +function defaultBrowsersToInstall(options) { + let executables = _server.registry.defaultExecutables(); + if (options.noShell) executables = executables.filter(e => e.name !== 'chromium-headless-shell'); + if (options.onlyShell) executables = executables.filter(e => e.name !== 'chromium'); + return executables; +} +function checkBrowsersToInstall(args, options) { + if (options.noShell && options.onlyShell) throw new Error(`Only one of --no-shell and --only-shell can be specified`); + const faultyArguments = []; + const executables = []; + const handleArgument = arg => { + const executable = _server.registry.findExecutable(arg); + if (!executable || executable.installType === 'none') faultyArguments.push(arg);else executables.push(executable); + if ((executable === null || executable === void 0 ? void 0 : executable.browserName) === 'chromium') executables.push(_server.registry.findExecutable('ffmpeg')); + }; + for (const arg of args) { + if (arg === 'chromium') { + if (!options.onlyShell) handleArgument('chromium'); + if (!options.noShell) handleArgument('chromium-headless-shell'); + } else { + handleArgument(arg); + } + } + if (faultyArguments.length) throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`); + return executables; +} +_utilsBundle.program.command('install [browser...]').description('ensure browsers necessary for this version of Playwright are installed').option('--with-deps', 'install system dependencies for browsers').option('--dry-run', 'do not execute installation, only print information').option('--force', 'force reinstall of stable browser channels').option('--only-shell', 'only install headless shell when installing chromium').option('--no-shell', 'do not install chromium headless shell').action(async function (args, options) { + // For '--no-shell' option, commander sets `shell: false` instead. + if (options.shell === false) options.noShell = true; + if ((0, _utils.isLikelyNpxGlobal)()) { + console.error((0, _utils.wrapInASCIIBox)([`WARNING: It looks like you are running 'npx playwright install' without first`, `installing your project's dependencies.`, ``, `To avoid unexpected behavior, please install your dependencies first, and`, `then run Playwright's install command:`, ``, ` npm install`, ` npx playwright install`, ``, `If your project does not yet depend on Playwright, first install the`, `applicable npm package (most commonly @playwright/test), and`, `then run Playwright's install command to download the browsers:`, ``, ` npm install @playwright/test`, ` npx playwright install`, ``].join('\n'), 1)); + } + try { + const hasNoArguments = !args.length; + const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options); + if (options.withDeps) await _server.registry.installDeps(executables, !!options.dryRun); + if (options.dryRun) { + for (const executable of executables) { + var _executable$directory, _executable$downloadU; + const version = executable.browserVersion ? `version ` + executable.browserVersion : ''; + console.log(`browser: ${executable.name}${version ? ' ' + version : ''}`); + console.log(` Install location: ${(_executable$directory = executable.directory) !== null && _executable$directory !== void 0 ? _executable$directory : ''}`); + if ((_executable$downloadU = executable.downloadURLs) !== null && _executable$downloadU !== void 0 && _executable$downloadU.length) { + const [url, ...fallbacks] = executable.downloadURLs; + console.log(` Download url: ${url}`); + for (let i = 0; i < fallbacks.length; ++i) console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`); + } + console.log(``); + } + } else { + const forceReinstall = hasNoArguments ? false : !!options.force; + await _server.registry.install(executables, forceReinstall); + await _server.registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || 'javascript').catch(e => { + e.name = 'Playwright Host validation warning'; + console.error(e); + }); + } + } catch (e) { + console.log(`Failed to install browsers\n${e}`); + (0, _utils.gracefullyProcessExitDoNotHang)(1); + } +}).addHelpText('afterAll', ` + +Examples: + - $ install + Install default browsers. + + - $ install chrome firefox + Install custom browsers, supports ${suggestedBrowsersToInstall()}.`); +_utilsBundle.program.command('uninstall').description('Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.').option('--all', 'Removes all browsers used by any Playwright installation from the system.').action(async options => { + delete process.env.PLAYWRIGHT_SKIP_BROWSER_GC; + await _server.registry.uninstall(!!options.all).then(({ + numberOfBrowsersLeft + }) => { + if (!options.all && numberOfBrowsersLeft > 0) { + console.log('Successfully uninstalled Playwright browsers for the current Playwright installation.'); + console.log(`There are still ${numberOfBrowsersLeft} browsers left, used by other Playwright installations.\nTo uninstall Playwright browsers for all installations, re-run with --all flag.`); + } + }).catch(logErrorAndExit); +}); +_utilsBundle.program.command('install-deps [browser...]').description('install dependencies necessary to run browsers (will ask for sudo permissions)').option('--dry-run', 'Do not execute installation commands, only print them').action(async function (args, options) { + try { + if (!args.length) await _server.registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);else await _server.registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun); + } catch (e) { + console.log(`Failed to install browser dependencies\n${e}`); + (0, _utils.gracefullyProcessExitDoNotHang)(1); + } +}).addHelpText('afterAll', ` +Examples: + - $ install-deps + Install dependencies for default browsers. + + - $ install-deps chrome firefox + Install dependencies for specific browsers, supports ${suggestedBrowsersToInstall()}.`); +const browsers = [{ + alias: 'cr', + name: 'Chromium', + type: 'chromium' +}, { + alias: 'ff', + name: 'Firefox', + type: 'firefox' +}, { + alias: 'wk', + name: 'WebKit', + type: 'webkit' +}]; +for (const { + alias, + name, + type +} of browsers) { + commandWithOpenOptions(`${alias} [url]`, `open page in ${name}`, []).action(function (url, options) { + open({ + ...options, + browser: type + }, url, options.target).catch(logErrorAndExit); + }).addHelpText('afterAll', ` +Examples: + + $ ${alias} https://example.com`); +} +commandWithOpenOptions('screenshot ', 'capture a page screenshot', [['--wait-for-selector ', 'wait for selector before taking a screenshot'], ['--wait-for-timeout ', 'wait for timeout in milliseconds before taking a screenshot'], ['--full-page', 'whether to take a full page screenshot (entire scrollable area)']]).action(function (url, filename, command) { + screenshot(command, command, url, filename).catch(logErrorAndExit); +}).addHelpText('afterAll', ` +Examples: + + $ screenshot -b webkit https://example.com example.png`); +commandWithOpenOptions('pdf ', 'save page as pdf', [['--wait-for-selector ', 'wait for given selector before saving as pdf'], ['--wait-for-timeout ', 'wait for given timeout in milliseconds before saving as pdf']]).action(function (url, filename, options) { + pdf(options, options, url, filename).catch(logErrorAndExit); +}).addHelpText('afterAll', ` +Examples: + + $ pdf https://example.com example.pdf`); +_utilsBundle.program.command('run-driver', { + hidden: true +}).action(function (options) { + (0, _driver.runDriver)(); +}); +_utilsBundle.program.command('run-server', { + hidden: true +}).option('--port ', 'Server port').option('--host ', 'Server host').option('--path ', 'Endpoint Path', '/').option('--max-clients ', 'Maximum clients').option('--mode ', 'Server mode, either "default" or "extension"').action(function (options) { + (0, _driver.runServer)({ + port: options.port ? +options.port : undefined, + host: options.host, + path: options.path, + maxConnections: options.maxClients ? +options.maxClients : Infinity, + extension: options.mode === 'extension' || !!process.env.PW_EXTENSION_MODE + }).catch(logErrorAndExit); +}); +_utilsBundle.program.command('print-api-json', { + hidden: true +}).action(function (options) { + (0, _driver.printApiJson)(); +}); +_utilsBundle.program.command('launch-server', { + hidden: true +}).requiredOption('--browser ', 'Browser name, one of "chromium", "firefox" or "webkit"').option('--config ', 'JSON file with launchServer options').action(function (options) { + (0, _driver.launchBrowserServer)(options.browser, options.config); +}); +_utilsBundle.program.command('show-trace [trace...]').option('-b, --browser ', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium').option('-h, --host ', 'Host to serve trace on; specifying this option opens trace in a browser tab').option('-p, --port ', 'Port to serve trace on, 0 for any free port; specifying this option opens trace in a browser tab').option('--stdin', 'Accept trace URLs over stdin to update the viewer').description('show trace viewer').action(function (traces, options) { + if (options.browser === 'cr') options.browser = 'chromium'; + if (options.browser === 'ff') options.browser = 'firefox'; + if (options.browser === 'wk') options.browser = 'webkit'; + const openOptions = { + host: options.host, + port: +options.port, + isServer: !!options.stdin + }; + if (options.port !== undefined || options.host !== undefined) (0, _traceViewer.runTraceInBrowser)(traces, openOptions).catch(logErrorAndExit);else (0, _traceViewer.runTraceViewerApp)(traces, options.browser, openOptions, true).catch(logErrorAndExit); +}).addHelpText('afterAll', ` +Examples: + + $ show-trace https://example.com/trace.zip`); +async function launchContext(options, extraOptions) { + validateOptions(options); + const browserType = lookupBrowserType(options); + const launchOptions = extraOptions; + if (options.channel) launchOptions.channel = options.channel; + launchOptions.handleSIGINT = false; + const contextOptions = + // Copy the device descriptor since we have to compare and modify the options. + options.device ? { + ...playwright.devices[options.device] + } : {}; + + // In headful mode, use host device scale factor for things to look nice. + // In headless, keep things the way it works in Playwright by default. + // Assume high-dpi on MacOS. TODO: this is not perfect. + if (!extraOptions.headless) contextOptions.deviceScaleFactor = _os.default.platform() === 'darwin' ? 2 : 1; + + // Work around the WebKit GTK scrolling issue. + if (browserType.name() === 'webkit' && process.platform === 'linux') { + delete contextOptions.hasTouch; + delete contextOptions.isMobile; + } + if (contextOptions.isMobile && browserType.name() === 'firefox') contextOptions.isMobile = undefined; + if (options.blockServiceWorkers) contextOptions.serviceWorkers = 'block'; + + // Proxy + + if (options.proxyServer) { + launchOptions.proxy = { + server: options.proxyServer + }; + if (options.proxyBypass) launchOptions.proxy.bypass = options.proxyBypass; + } + const browser = await browserType.launch(launchOptions); + if (process.env.PWTEST_CLI_IS_UNDER_TEST) { + process._didSetSourcesForTest = text => { + process.stdout.write('\n-------------8<-------------\n'); + process.stdout.write(text); + process.stdout.write('\n-------------8<-------------\n'); + const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN; + if (autoExitCondition && text.includes(autoExitCondition)) closeBrowser(); + }; + // Make sure we exit abnormally when browser crashes. + const logs = []; + require('playwright-core/lib/utilsBundle').debug.log = (...args) => { + const line = require('util').format(...args) + '\n'; + logs.push(line); + process.stderr.write(line); + }; + browser.on('disconnected', () => { + const hasCrashLine = logs.some(line => line.includes('process did exit:') && !line.includes('process did exit: exitCode=0, signal=null')); + if (hasCrashLine) { + process.stderr.write('Detected browser crash.\n'); + (0, _utils.gracefullyProcessExitDoNotHang)(1); + } + }); + } + + // Viewport size + if (options.viewportSize) { + try { + const [width, height] = options.viewportSize.split(',').map(n => parseInt(n, 10)); + contextOptions.viewport = { + width, + height + }; + } catch (e) { + throw new Error('Invalid viewport size format: use "width, height", for example --viewport-size=800,600'); + } + } + + // Geolocation + + if (options.geolocation) { + try { + const [latitude, longitude] = options.geolocation.split(',').map(n => parseFloat(n.trim())); + contextOptions.geolocation = { + latitude, + longitude + }; + } catch (e) { + throw new Error('Invalid geolocation format, should be "lat,long". For example --geolocation="37.819722,-122.478611"'); + } + contextOptions.permissions = ['geolocation']; + } + + // User agent + + if (options.userAgent) contextOptions.userAgent = options.userAgent; + + // Lang + + if (options.lang) contextOptions.locale = options.lang; + + // Color scheme + + if (options.colorScheme) contextOptions.colorScheme = options.colorScheme; + + // Timezone + + if (options.timezone) contextOptions.timezoneId = options.timezone; + + // Storage + + if (options.loadStorage) contextOptions.storageState = options.loadStorage; + if (options.ignoreHttpsErrors) contextOptions.ignoreHTTPSErrors = true; + + // HAR + + if (options.saveHar) { + contextOptions.recordHar = { + path: _path.default.resolve(process.cwd(), options.saveHar), + mode: 'minimal' + }; + if (options.saveHarGlob) contextOptions.recordHar.urlFilter = options.saveHarGlob; + contextOptions.serviceWorkers = 'block'; + } + + // Close app when the last window closes. + + const context = await browser.newContext(contextOptions); + let closingBrowser = false; + async function closeBrowser() { + // We can come here multiple times. For example, saving storage creates + // a temporary page and we call closeBrowser again when that page closes. + if (closingBrowser) return; + closingBrowser = true; + if (options.saveTrace) await context.tracing.stop({ + path: options.saveTrace + }); + if (options.saveStorage) await context.storageState({ + path: options.saveStorage + }).catch(e => null); + if (options.saveHar) await context.close(); + await browser.close(); + } + context.on('page', page => { + page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed. + page.on('close', () => { + const hasPage = browser.contexts().some(context => context.pages().length > 0); + if (hasPage) return; + // Avoid the error when the last page is closed because the browser has been closed. + closeBrowser().catch(() => {}); + }); + }); + process.on('SIGINT', async () => { + await closeBrowser(); + (0, _utils.gracefullyProcessExitDoNotHang)(130); + }); + const timeout = options.timeout ? parseInt(options.timeout, 10) : 0; + context.setDefaultTimeout(timeout); + context.setDefaultNavigationTimeout(timeout); + if (options.saveTrace) await context.tracing.start({ + screenshots: true, + snapshots: true + }); + + // Omit options that we add automatically for presentation purpose. + delete launchOptions.headless; + delete launchOptions.executablePath; + delete launchOptions.handleSIGINT; + delete contextOptions.deviceScaleFactor; + return { + browser, + browserName: browserType.name(), + context, + contextOptions, + launchOptions + }; +} +async function openPage(context, url) { + const page = await context.newPage(); + if (url) { + if (_fs.default.existsSync(url)) url = 'file://' + _path.default.resolve(url);else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:')) url = 'http://' + url; + await page.goto(url).catch(error => { + if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && (0, _errors.isTargetClosedError)(error)) { + // Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting + // in a stray navigation aborted error. We should ignore it. + } else { + throw error; + } + }); + } + return page; +} +async function open(options, url, language) { + const { + context, + launchOptions, + contextOptions + } = await launchContext(options, { + headless: !!process.env.PWTEST_CLI_HEADLESS, + executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH + }); + await context._enableRecorder({ + language, + launchOptions, + contextOptions, + device: options.device, + saveStorage: options.saveStorage, + handleSIGINT: false + }); + await openPage(context, url); +} +async function codegen(options, url) { + const { + target: language, + output: outputFile, + testIdAttribute: testIdAttributeName + } = options; + const tracesDir = _path.default.join(_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`); + const { + context, + launchOptions, + contextOptions + } = await launchContext(options, { + headless: !!process.env.PWTEST_CLI_HEADLESS, + executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH, + tracesDir + }); + _utilsBundle.dotenv.config({ + path: 'playwright.env' + }); + await context._enableRecorder({ + language, + launchOptions, + contextOptions, + device: options.device, + saveStorage: options.saveStorage, + mode: 'recording', + codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions', + testIdAttributeName, + outputFile: outputFile ? _path.default.resolve(outputFile) : undefined, + handleSIGINT: false + }); + await openPage(context, url); +} +async function waitForPage(page, captureOptions) { + if (captureOptions.waitForSelector) { + console.log(`Waiting for selector ${captureOptions.waitForSelector}...`); + await page.waitForSelector(captureOptions.waitForSelector); + } + if (captureOptions.waitForTimeout) { + console.log(`Waiting for timeout ${captureOptions.waitForTimeout}...`); + await page.waitForTimeout(parseInt(captureOptions.waitForTimeout, 10)); + } +} +async function screenshot(options, captureOptions, url, path) { + const { + context + } = await launchContext(options, { + headless: true + }); + console.log('Navigating to ' + url); + const page = await openPage(context, url); + await waitForPage(page, captureOptions); + console.log('Capturing screenshot into ' + path); + await page.screenshot({ + path, + fullPage: !!captureOptions.fullPage + }); + // launchContext takes care of closing the browser. + await page.close(); +} +async function pdf(options, captureOptions, url, path) { + if (options.browser !== 'chromium') throw new Error('PDF creation is only working with Chromium'); + const { + context + } = await launchContext({ + ...options, + browser: 'chromium' + }, { + headless: true + }); + console.log('Navigating to ' + url); + const page = await openPage(context, url); + await waitForPage(page, captureOptions); + console.log('Saving as pdf into ' + path); + await page.pdf({ + path + }); + // launchContext takes care of closing the browser. + await page.close(); +} +function lookupBrowserType(options) { + let name = options.browser; + if (options.device) { + const device = playwright.devices[options.device]; + name = device.defaultBrowserType; + } + let browserType; + switch (name) { + case 'chromium': + browserType = playwright.chromium; + break; + case 'webkit': + browserType = playwright.webkit; + break; + case 'firefox': + browserType = playwright.firefox; + break; + case 'cr': + browserType = playwright.chromium; + break; + case 'wk': + browserType = playwright.webkit; + break; + case 'ff': + browserType = playwright.firefox; + break; + } + if (browserType) return browserType; + _utilsBundle.program.help(); +} +function validateOptions(options) { + if (options.device && !(options.device in playwright.devices)) { + const lines = [`Device descriptor not found: '${options.device}', available devices are:`]; + for (const name in playwright.devices) lines.push(` "${name}"`); + throw new Error(lines.join('\n')); + } + if (options.colorScheme && !['light', 'dark'].includes(options.colorScheme)) throw new Error('Invalid color scheme, should be one of "light", "dark"'); +} +function logErrorAndExit(e) { + if (process.env.PWDEBUGIMPL) console.error(e);else console.error(e.name + ': ' + e.message); + (0, _utils.gracefullyProcessExitDoNotHang)(1); +} +function codegenId() { + return process.env.PW_LANG_NAME || 'playwright-test'; +} +function commandWithOpenOptions(command, description, options) { + let result = _utilsBundle.program.command(command).description(description); + for (const option of options) result = result.option(option[0], ...option.slice(1)); + return result.option('-b, --browser ', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium').option('--block-service-workers', 'block service workers').option('--channel ', 'Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc').option('--color-scheme ', 'emulate preferred color scheme, "light" or "dark"').option('--device ', 'emulate device, for example "iPhone 11"').option('--geolocation ', 'specify geolocation coordinates, for example "37.819722,-122.478611"').option('--ignore-https-errors', 'ignore https errors').option('--load-storage ', 'load context storage state from the file, previously saved with --save-storage').option('--lang ', 'specify language / locale, for example "en-GB"').option('--proxy-server ', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option('--proxy-bypass ', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option('--save-har ', 'save HAR file with all network activity at the end').option('--save-har-glob ', 'filter entries in the HAR by matching url against this glob pattern').option('--save-storage ', 'save context storage state at the end, for later use with --load-storage').option('--timezone