From 3433c0bda6af5fbecc1411d15125b6592d9cadf8 Mon Sep 17 00:00:00 2001 From: Wolf Date: Tue, 4 Jun 2024 19:19:40 +0100 Subject: [PATCH] The initial commit --- .github/CODEOWNERS | 5 + .github/CODE_OF_CONDUCT.md | 75 ++++++++ .github/CONTRIBUTING.md | 14 ++ .github/FUNDING.yml | 4 + .github/ISSUE_TEMPLATE/ask_question.yml | 22 +++ .github/ISSUE_TEMPLATE/bug_report.yml | 58 +++++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 43 +++++ .github/PULL_REQUEST_TEMPLATE.md | 36 ++++ .github/SECURITY.md | 40 +++++ .github/dependabot.yml | 18 ++ .github/scripts/check-jobs.sh | 38 +++++ .github/workflows/cicd.yml | 48 ++++++ .github/workflows/citation-validation.yml | 84 +++++++++ .../workflows/delete-old-workflow-runs.yml | 75 ++++++++ .github/workflows/dependabot.yml | 53 ++++++ .github/workflows/document-validation.yml | 102 +++++++++++ .github/workflows/generate-release.yml | 62 +++++++ .github/workflows/generate-test-release.yml | 60 +++++++ .github/workflows/greetings.yml | 27 +++ .../purge-deprecated-workflow-runs.yml | 47 +++++ .github/workflows/repository-validation.yml | 100 +++++++++++ .github/workflows/security-hardening.yml | 33 ++++ .github/workflows/stale.yml | 57 +++++++ .gitignore | 11 ++ .yamllint | 26 +++ CITATION.cff | 15 ++ LICENSE.md | 25 +++ README.md | 108 ++++++++++++ screenshots/execute-stack.png | Bin 0 -> 42481 bytes screenshots/source-stack.png | Bin 0 -> 19398 bytes screenshots/unbound.png | Bin 0 -> 10663 bytes src/trapper.sh | 161 ++++++++++++++++++ tests/execute-stack/child-1.sh | 18 ++ tests/execute-stack/child-2.sh | 17 ++ tests/execute-stack/parent.sh | 18 ++ tests/source-stack/child-1.sh | 16 ++ tests/source-stack/child-2.sh | 20 +++ tests/source-stack/parent.sh | 18 ++ tests/unbounded/unbounded.sh | 19 +++ 40 files changed, 1581 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/ask_question.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100755 .github/scripts/check-jobs.sh create mode 100644 .github/workflows/cicd.yml create mode 100644 .github/workflows/citation-validation.yml create mode 100644 .github/workflows/delete-old-workflow-runs.yml create mode 100644 .github/workflows/dependabot.yml create mode 100644 .github/workflows/document-validation.yml create mode 100644 .github/workflows/generate-release.yml create mode 100644 .github/workflows/generate-test-release.yml create mode 100644 .github/workflows/greetings.yml create mode 100644 .github/workflows/purge-deprecated-workflow-runs.yml create mode 100644 .github/workflows/repository-validation.yml create mode 100644 .github/workflows/security-hardening.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .gitignore create mode 100644 .yamllint create mode 100644 CITATION.cff create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 screenshots/execute-stack.png create mode 100644 screenshots/source-stack.png create mode 100644 screenshots/unbound.png create mode 100644 src/trapper.sh create mode 100755 tests/execute-stack/child-1.sh create mode 100755 tests/execute-stack/child-2.sh create mode 100755 tests/execute-stack/parent.sh create mode 100755 tests/source-stack/child-1.sh create mode 100755 tests/source-stack/child-2.sh create mode 100755 tests/source-stack/parent.sh create mode 100755 tests/unbounded/unbounded.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..50be806 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# +# These owners will be the default owners for everything in the repo. +# +* @TGWolf + diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5ffa3a8 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behaviour that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behaviour by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behaviour and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behaviour. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org), version 1.4, +available at + +For answers to common questions about this code of conduct, see + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..145a864 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +Please refer to the +[contributing](https://github.com/WolfSoftware/contributing) +documentation. + +## Important + +ALL commits must be signed to ensure the identity of the developer, any pull +requests that are made with unsigned commits will be rejected as a matter of +course. + +> This project has a [code of conduct](CODE_OF_CONDUCT.md). By interacting +with this repository, organization, or community you agree to abide by its terms. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b7a1e2f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# Funding +# https://help.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository + +github: [WolfSoftware,TGWolf] diff --git a/.github/ISSUE_TEMPLATE/ask_question.yml b/.github/ISSUE_TEMPLATE/ask_question.yml new file mode 100644 index 0000000..b655dd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask_question.yml @@ -0,0 +1,22 @@ +name: Ask a question +description: If you don't have a specific issue or bug to report you can still ask us questions and we will do our best to answer them +title: "[Question]: " +labels: ["type: question", "state: triage"] +assignees: + - tgwolf +body: + - type: textarea + id: question + attributes: + label: What is your question? + description: Please give us time to review your question and formulate an answer. + validations: + required: true + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/DevelopersToolbox/trapper/blob/master/.github/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..6b636b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: Report a bug +description: Found a bug? Let us knonw what the issue is and we will attempt to fix it +title: "[Bug]: " +labels: ["type: bug", "state: triage"] +assignees: + - tgwolf +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: How do we reproduct the bug? + description: What are the steps we need to take to reproduce the behavior? + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: false + - type: textarea + id: screenshoots + attributes: + label: Screeenshots + description: Upload any screenshots that might help demonstrate the bug. + validations: + required: false + - type: textarea + id: additional-information + attributes: + label: Additional information + description: Please provide any additional information that you think will help us to resolve this bug. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/DevelopersToolbox/trapper/blob/master/.github/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..cabd7e8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support us + url: https://ko-fi.com/wolfsoftware + about: Show your support + - name: Visit our website + url: https://wolfsoftware.com/ + about: Visit the Wolf Software website and see what else we do and what services we offer diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..42e9715 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +name: Request a new feature +description: Got an idea for a new feature? Let us know what you want and we will see if we can add it +title: "[Feature Request]: " +labels: ["type: feature", "state: triage"] +assignees: + - tgwolf +body: + - type: textarea + id: releated-to + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of what the problem is. E.g. I'm always frustrated when ... + validations: + required: true + - type: textarea + id: suggested-solution + attributes: + label: Suggested Solution + description: A clear and concise description of what you want to see implemented. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + id: additional-information + attributes: + label: Additional information + description: Please provide any additional information that you think will help us to resolve this bug. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/DevelopersToolbox/trapper/blob/master/.github/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0e99242 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ +# Thank You + +Thanks for submitting a pull request! Please provide enough information so that +others can review your pull request: + +## Summary + + + +This PR fixes/implements the following **bugs/features** + +* [ ] Bug 1 +* [ ] Feature 1 +* [ ] Breaking changes + + + +Explain the **motivation** for making this change. What existing problem does +the pull request solve? + + + +## Test plan (required) + +Demonstrate the code is solid. Example: The exact commands you ran and their +output, screenshots help greatly. + + + +## Closing issues + + +Fixes # diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..b46843b --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for this project. + +* [Reporting a Bug](#reporting-a-bug) +* [Disclosure Policy](#disclosure-policy) +* [Comments on this Policy](#comments-on-this-policy) + +## Reporting a Bug + +We take **ALL** security related bugs and issues very seriously. + +If you think you have identified a security related issue, please +[report it immediately](mailto:disclose@wolfsoftware.com) and include +the word "SECURITY" in the subject line. If you are not sure, don’t worry. +Better safe than sorry – just send an email. + +* Please provide as much information as you can. +* Please do not open issues related to any security concerns publicly. +* Please do not include anyone else on the disclosure email. + +Report security bugs in third-party modules to the person or team maintaining +the module. + +## Disclosure Policy + +When a security report is received, we will carry out the following steps: + +* Confirm the problem and determine the affected versions. +* Audit code to find any potential similar problems. +* Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible. + +We will endeavour to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c9f5bfa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 10 + commit-message: + prefix: "chore:" + labels: + - "dependabot: ecosystem : github actions" + - "dependabot: dependencies" + assignees: + - "TGWolf" + diff --git a/.github/scripts/check-jobs.sh b/.github/scripts/check-jobs.sh new file mode 100755 index 0000000..e35587c --- /dev/null +++ b/.github/scripts/check-jobs.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# This script receives a JSON string containing job results and checks for any failures. + +# Check if jq is available +if ! command -v jq &> /dev/null; then + echo "jq could not be found, please install jq to run this script." + exit 1 +fi + +# Read the JSON string from the first script argument +job_results_json=$1 + +# Check if the job results JSON is not empty +if [[ -z "$job_results_json" ]]; then + echo "No job results JSON provided." + exit 1 +fi + +# Set default state +failed_jobs=false + +# Use jq to parse the JSON and check each job's result +while IFS= read -r line; do + job_name=$(echo "$line" | awk '{print $1}') + result=$(echo "$line" | awk '{print $3}') + + if [ "$result" != "success" ]; then + echo "$job_name failed." + failed_jobs=true + else + echo "$job_name succeed." + fi +done <<< "$( echo "$job_results_json" | jq -r 'to_entries[] | "\(.key) result: \(.value.result)"' )" + +if [ "$failed_jobs" = true ] ; then + exit 1 +fi diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..2fe035c --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,48 @@ +name: CI/CD Pipeline + +on: + push: + branches-ignore: + - 'dependabot/**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + + pull_request: + branches: + - '**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Perform ShellCheck Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/shellcheck/master/pipeline.sh) + + cicd-pipeline: + if: always() + name: CI/CD Pipeline + needs: + - shellcheck + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/citation-validation.yml b/.github/workflows/citation-validation.yml new file mode 100644 index 0000000..919e944 --- /dev/null +++ b/.github/workflows/citation-validation.yml @@ -0,0 +1,84 @@ +name: Citation Validation + +on: + push: + branches-ignore: + - 'dependabot/**' + paths: + - 'CITATION.cff' + pull_request: + branches: + - '**' + paths: + - 'CITATION.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-ruby-version: + name: Get Latest Ruby Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "ruby" + highest-only: true + remove-patch-version: true + + awesomebot: + name: Awesomebot + needs: get-ruby-version + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Ruby ${{ needs.get-ruby-version.outputs.version }} + uses: ruby/setup-ruby@d5fb7a202fc07872cb44f00ba8e6197b70cb0c55 # v1.179.0 + with: + ruby-version: ${{ needs.get-ruby-version.outputs.version }} + + - name: Perform Awesomebot Analysis + env: + FLAGS: "default" + WHITELIST: "https://img.shields.io" + INCLUDE_FILES: "CITATION.cff" + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/awesomebot/master/pipeline.sh) + + validate-citation-file: + name: Validate CITATION.cff + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Validate CITATION.cff + uses: citation-file-format/cffconvert-github-action@4cf11baa70a673bfdf9dad0acc7ee33b3f4b6084 # v2.0.0 + with: + args: "--validate" + + citation-validation-pipeline: + if: always() + name: Citation Validation Pipeline + needs: + - awesomebot + - validate-citation-file + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/delete-old-workflow-runs.yml b/.github/workflows/delete-old-workflow-runs.yml new file mode 100644 index 0000000..8786738 --- /dev/null +++ b/.github/workflows/delete-old-workflow-runs.yml @@ -0,0 +1,75 @@ +name: Delete Old Workflow Runs + +on: + workflow_dispatch: + inputs: + days: + description: 'Number of days to retain workflow runs.' + required: true + default: '30' + minimum-runs: + description: 'The minimum number of runs to keep for each workflow.' + required: true + default: '6' + + schedule: + - cron: '31 2 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DAYS: 30 + MINIMUM_RUNS: 6 + +permissions: read-all + +jobs: + set-runtime-values: + name: Set Runtime Values + runs-on: ubuntu-latest + outputs: + days: ${{ steps.set-output-defaults.outputs.days }} + minimum-runs: ${{ steps.set-output-defaults.outputs.minimum-runs }} + + steps: + - name: Set Runtime Values + id: set-output-defaults + run: | + echo "days=${{ github.event.inputs.days || env.DAYS }}" >> "${GITHUB_OUTPUT}" + echo "minimum-runs=${{ github.event.inputs.minimum-runs || env.MINIMUM_RUNS }}" >> "${GITHUB_OUTPUT}" + + delete-old-workflows: + name: Delete Old Workflow Runs + runs-on: ubuntu-latest + permissions: + actions: write + needs: + - set-runtime-values + + steps: + - name: Delete Old Workflow Runs + uses: Mattraks/delete-workflow-runs@39f0bbed25d76b34de5594dceab824811479e5de # v2.0.6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + retain_days: ${{ needs.set-runtime-values.outputs.days }} + keep_minimum_runs: ${{ needs.set-runtime-values.outputs.minimum-runs }} + + slack-workflow-status: + if: always() + name: Slack Post Workflow Notification + needs: + - delete-old-workflows + runs-on: ubuntu-latest + + steps: + - name: Slack Workflow Notifications + if: ${{ github.event_name == 'schedule' && needs.delete-old-workflows.result != 'success'}} + uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a # v1.3.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + include_jobs: on-failure + include_commit_message: true diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 0000000..247ba9d --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,53 @@ +name: Dependabot Pull Request Approve & Merge + +on: pull_request + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + dependabot: + name: Dependabot + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Fetch Metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@5e5f99653a5b510e8555840e80cbf1514ad4af38 # v2.1.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Approve PR + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' || steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor' }} + run: | + gh pr review --approve "${PR_URL}" -b "I'm **approving** this pull request because it includes a patch or minor update" + gh pr edit "${PR_URL}" --add-label "dependabot: auto approve" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-Merge Non-Major Updates + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' || steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor' }} + run: | + gh pr comment "${PR_URL}" --body "I'm automatically merging this PR because it includes a patch or minor update" + gh pr merge --auto --squash --delete-branch "${PR_URL}" + gh pr edit "${PR_URL}" --add-label "dependabot: auto merge" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Comment & Label Major Updates + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-major' }} + run: | + gh pr comment "${PR_URL}" --body "I'm **NOT** automatically merging this PR because it includes a major update of a dependency" + gh pr edit "${PR_URL}" --add-label "dependabot: manual merge" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/document-validation.yml b/.github/workflows/document-validation.yml new file mode 100644 index 0000000..6b65173 --- /dev/null +++ b/.github/workflows/document-validation.yml @@ -0,0 +1,102 @@ +name: Documentation Validation + +on: + push: + branches-ignore: + - 'dependabot/**' + paths: + - '**/*.md' + pull_request: + branches: + - '**' + paths: + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-node-version: + name: Get Latest Node Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "node" + highest-only: true + remove-patch-version: true + + get-ruby-version: + name: Get Latest Ruby Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Version + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # v0.1.3 + id: get-language-versions + with: + language: "ruby" + highest-only: true + remove-patch-version: true + + awesomebot: + name: Awesomebot + needs: get-ruby-version + runs-on: ubuntu-latest + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Ruby ${{ needs.get-ruby-version.outputs.version }} + uses: ruby/setup-ruby@d5fb7a202fc07872cb44f00ba8e6197b70cb0c55 # v1.179.0 + with: + ruby-version: ${{ needs.get-ruby-version.outputs.version }} + + - name: Perform Awesomebot Analysis + env: + FLAGS: "default" + WHITELIST: "https://img.shields.io" + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/awesomebot/master/pipeline.sh) + + markdown-lint: + name: Markdown Lint + needs: get-node-version + runs-on: ubuntu-latest + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Setup Node ${{ needs.get-node-version.outputs.version }} + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: ${{ needs.get-node-version.outputs.version }} + + - name: Perform Markdown Lint Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/markdown-lint/master/pipeline.sh) + env: + EXCLUDE_FILES: "README.md,CHANGELOG.md" + + repository-validation-pipeline: + if: always() + name: Documentation Validation Pipeline + needs: + - awesomebot + - markdown-lint + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/generate-release.yml b/.github/workflows/generate-release.yml new file mode 100644 index 0000000..37682a4 --- /dev/null +++ b/.github/workflows/generate-release.yml @@ -0,0 +1,62 @@ +name: Generate a Release + +on: + push: + tags: + - 'v[0-9].[0-9]+.[0-9]+' + - '!v[0-9].[0-9]+.[0-9]+rc[0-9]+' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + set-release-version: + name: Set Release Version + runs-on: ubuntu-latest + outputs: + release-version: ${{ steps.set-release-version.outputs.release-version }} + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Set the Release Version + id: set-release-version + run: | + echo "release-version=${GITHUB_REF#refs/*/}" >> "${GITHUB_OUTPUT}" + + create-release: + name: Create a Release + permissions: + contents: write + runs-on: ubuntu-latest + needs: + - set-release-version + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Generate Changelog + uses: Bullrich/generate-release-changelog@6b60f004b4bf12ff271603dc32dbd261965ad2f2 # v2.0.2 + id: Changelog + env: + REPO: ${{ github.repository }} + + - name: Create a Release + id: create_release + uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref }} + name: ${{ needs.set-release-version.outputs.release-version }} + body: ${{ steps.Changelog.outputs.changelog }} + draft: false + prerelease: false diff --git a/.github/workflows/generate-test-release.yml b/.github/workflows/generate-test-release.yml new file mode 100644 index 0000000..b4b3fe7 --- /dev/null +++ b/.github/workflows/generate-test-release.yml @@ -0,0 +1,60 @@ +name: Generate a TEST Release + +on: + push: + tags: + - 'v[0-9].[0-9]+.[0-9]+rc[0-9]+' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + set-release-version: + name: Set Release Version + runs-on: ubuntu-latest + outputs: + release-version: ${{ steps.set-release-version.outputs.release-version }} + + steps: + - name: Checkout the repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Set the release version + id: set-release-version + run: echo "release-version=${GITHUB_REF#refs/*/}" >> "${GITHUB_OUTPUT}" + + create-release: + name: Create Release + permissions: + contents: write + runs-on: ubuntu-latest + needs: + - set-release-version + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + + - name: Generate Changelog + uses: Bullrich/generate-release-changelog@6b60f004b4bf12ff271603dc32dbd261965ad2f2 # v2.0.2 + id: Changelog + env: + REPO: ${{ github.repository }} + + - name: Create a Release + id: create_release + uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref }} + name: ${{ needs.set-release-version.outputs.release-version }} + body: ${{ steps.Changelog.outputs.changelog }} + draft: false + prerelease: true diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 0000000..ddd7cd8 --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,27 @@ +name: Greetings + +on: + pull_request: + issues: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + greetings: + name: Handle Greetings + permissions: + issues: write + pull-requests: write + runs-on: ubuntu-latest + + steps: + - name: Handle Greetings + uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 # v1.3.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: "Thank you for raising your first issue - all contributions to this project are welcome!" + pr-message: "Thank you for raising your first pull request - all contributions to this project are welcome!" diff --git a/.github/workflows/purge-deprecated-workflow-runs.yml b/.github/workflows/purge-deprecated-workflow-runs.yml new file mode 100644 index 0000000..f3cb27a --- /dev/null +++ b/.github/workflows/purge-deprecated-workflow-runs.yml @@ -0,0 +1,47 @@ +name: Purge Deprecated Workflow Runs + +on: + workflow_dispatch: + + schedule: + - cron: '57 5 * * 1' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + purge-obsolete-workflows: + name: Purge Deprecated Workflow Runs + permissions: + actions: write + runs-on: ubuntu-latest + + steps: + - name: Purge Deprecated Workflow Runs + uses: otto-de/purge-deprecated-workflow-runs@31a4e821d43e9a354cbd65845922c76e4b4b3633 # v 2.0.4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + remove-obsolete: true + remove-cancelled: true + remove-failed: true + remove-skipped: true + + slack-workflow-status: + if: always() + name: Slack Post Workflow Notification + needs: + - purge-obsolete-workflows + runs-on: ubuntu-latest + + steps: + - name: Slack Workflow Notifications + if: ${{ github.event_name == 'schedule' && needs.purge-obsolete-workflows.result != 'success'}} + uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a # v1.3.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + include_jobs: on-failure + include_commit_message: true diff --git a/.github/workflows/repository-validation.yml b/.github/workflows/repository-validation.yml new file mode 100644 index 0000000..8f0c10e --- /dev/null +++ b/.github/workflows/repository-validation.yml @@ -0,0 +1,100 @@ +name: Repository Validation + +on: + push: + branches-ignore: + - 'dependabot/**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + pull_request: + branches: + - '**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + get-go-version: + name: Get Latest Go Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Versions + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # V0.1.3 + id: get-language-versions + with: + language: "go" + highest-only: true + remove-patch-version: true + + get-python-version: + name: Get Latest Python Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-language-versions.outputs.latest-versions }} + + steps: + - name: Get Required Versions + uses: ActionsToolbox/get-language-versions-action@446919617fd774095b5dd3ed71c39dd3fd0d8f4f # V0.1.3 + id: get-language-versions + with: + language: "python" + highest-only: true + remove-patch-version: true + + action-lint: + name: Action Lint + needs: get-go-version + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # V4.1.6 + + - name: Setup Go ${{ needs.get-go-version.outputs.version }} + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # V5.0.1 + with: + go-version: ${{ needs.get-go-version.outputs.version }} + + - name: Perform Action Lint Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/action-lint/master/pipeline.sh) + + yaml-lint: + name: YAML Lint + needs: get-python-version + runs-on: ubuntu-latest + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # V4.1.6 + + - name: Set up Python ${{ needs.get-python-version.outputs.version }} + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # V5.1.0 + with: + python-version: ${{ needs.get-python-version.outputs.version }} + + - name: Perform YAML Lint Analysis + run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/yaml-lint/master/pipeline.sh) + + repository-validation-pipeline: + if: always() + name: Repository Validation Pipeline + needs: + - action-lint + - yaml-lint + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # V4.1.6 + + - name: Check Job Statuses + run: .github/scripts/check-jobs.sh '${{ toJson(needs) }}' diff --git a/.github/workflows/security-hardening.yml b/.github/workflows/security-hardening.yml new file mode 100644 index 0000000..42d828d --- /dev/null +++ b/.github/workflows/security-hardening.yml @@ -0,0 +1,33 @@ +name: Security Hardening + +on: + push: + branches-ignore: + - 'dependabot/**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + pull_request: + branches: + - '**' + paths-ignore: + - '**/*.md' + - '**/*.cff' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + security-hardening: + name: Harden Security + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Ensure SHA Pinned Actions + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@2f2ebc6d914ab515939dc13f570f91baeb2c194c # v3.0.6 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..9ea540b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,57 @@ +name: Stale Issue & PR Handler + +on: + schedule: + - cron: '2 3 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + stale: + name: Handle Stale Issues & PRs + permissions: + contents: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + + steps: + - name: Handle Stale Issues & PRs + uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + id: stale-issues + with: + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' + close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' + days-before-issue-stale: 30 + days-before-issue-close: 5 + stale-issue-label: 'state: stale' + close-issue-label: 'resolution: closed' + exempt-issue-labels: 'state: blocked,state: keep' + stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' + close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' + days-before-pr-stale: 45 + days-before-pr-close: 10 + stale-pr-label: 'state: stale' + close-pr-label: 'resolution: closed' + exempt-pr-labels: 'state: blocked,state: keep' + + slack-workflow-status: + if: always() + name: Slack Post Workflow Notification + needs: + - stale + runs-on: ubuntu-latest + + steps: + - name: Slack Workflow Notifications + if: ${{ github.event_name == 'schedule' && needs.stale.result != 'success'}} + uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a # v1.3.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + include_jobs: on-failure + include_commit_message: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20f4490 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +### Hard Coded Minimums ### +# +# Because I use a Mac +# +**/.DS-Store + +# +# Ignore Awesomebot output +# +**/ab-results* + diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..804e6c1 --- /dev/null +++ b/.yamllint @@ -0,0 +1,26 @@ +--- + +extends: default + +rules: + braces: + level: warning + max-spaces-inside: 1 + brackets: + level: warning + max-spaces-inside: 1 + colons: + level: warning + commas: + level: warning + comments-indentation: disable + document-start: disable + empty-lines: + level: warning + hyphens: + level: warning + indentation: + level: warning + indent-sequences: consistent + line-length: disable + truthy: disable diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..5bb2122 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,15 @@ +cff-version: 1.2.0 +message: If you use this software, please cite it using these metadata. +title: Error Trapper +abstract: An error trapper plugin for helping to debug bash scripts. +type: software +version: 0.1.0 +date-released: 2024-06-04 +repository-code: https://github.com/DevelopersToolbox/trapper +keywords: + - "Wolf Software" + - "Software" +license: MIT +authors: + - family-names: "Wolf" + orcid: "https://orcid.org/0009-0007-0983-2072" diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c14811f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,25 @@ +The MIT License (MIT) +===================== + +Copyright © `2009-2024` `Wolf Software` + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..41ba459 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ + +

+ + DevelopersToolbox logo + +
+ + Github Build Status + + + License + + + Created + +
+ + Release + + + Released + + + Commits since release + +
+ + + + + + + + + + + + +

+ +## Overview + +At Wolf Software we write a lot of bash scripts for many purposes and the one thing that we found lacking was a simple to use debugging tool. + +Trapper is something that weas developed originally for internal use to debug scripts we release with the aim being a simple plugin that required minimal changes to the original script. + +Trapper is capable of capturing a large array of runtime errors and attempts to point to where the error happened. + +## Usage + +Simply source trapper at the top of your script and then execute it as normal. + +Trapper works by setting a trap for any errors and attempts to display where the errors happened. + +> Truncated snippet +``` +set -Eeuo pipefail + +function trap_with_arg() +{ + func="$1"; + shift + + for sig ; do + # shellcheck disable=SC2064 + trap "$func $sig" "$sig" + done +} + +trap_with_arg 'failure ${?}' ERR EXIT +``` + +It is capable of detecting errors in a many different scenarios. + +| Scenario | Requirements | Results | +| ----------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- | +| Single script | Include trapper.sh | Reports filename, line number and code snippet. | +| Executing scripts | Include trapper.sh (in all scripts) | Reports filename, line number and code snippet for full stack trace (calling scripts). | +| Including scripts | Include trapper.sh (only in parent script) | Reports filename, line number and code snippet of the failing included script. | + +## Examples + +### Testing for unset (unbound) variables + +Single script attempt to use an unbound variable. + +[Source](tests/unbounded/unbounded.sh) + +![Unbounded](screenshots/unbound.png) + +### Testing execute stack (scripts calling scripts) + +Parent script executing child script with error in the final child. + +[Source](tests/execute-stack/parent.sh) + +![Execute Stack](screenshots/execute-stack.png) + +### Testing source stack (scripts including scripts) + +Parent script including (sourcing) child scripts with an error in the final child. + +[Source](tests/source-stack/parent.sh) + +![Execute Stack](screenshots/source-stack.png) + +
+

diff --git a/screenshots/execute-stack.png b/screenshots/execute-stack.png new file mode 100644 index 0000000000000000000000000000000000000000..29bf8b5d648c29ca251986a265d883afac50a593 GIT binary patch literal 42481 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 literal 0 HcmV?d00001 diff --git a/screenshots/source-stack.png b/screenshots/source-stack.png new file mode 100644 index 0000000000000000000000000000000000000000..199f0ae080be81610c76fdd8746d7de39cfb5c31 GIT binary patch literal 19398 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 RfXDS9P$dn;DtXJW{{;@Tmhu1q literal 0 HcmV?d00001 diff --git a/src/trapper.sh b/src/trapper.sh new file mode 100644 index 0000000..d6fdde7 --- /dev/null +++ b/src/trapper.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Trapper is designed to be a simple to use plugin to assist in debugging simple # +# or complex bash scripts. It will attempt to capture the error and display a code # +# snippet highlighting where the error is as well as provide the error message. # +# -------------------------------------------------------------------------------- # + +# -------------------------------------------------------------------------------- # +# Configure the shell. # +# -------------------------------------------------------------------------------- # + +set -Eeuo pipefail + +# -------------------------------------------------------------------------------- # +# Capture stderr so we can replay the error messages. # +# -------------------------------------------------------------------------------- # + +ERROR_FILE='/tmp/trapper' +exec 2>"${ERROR_FILE}" + +# -------------------------------------------------------------------------------- # +# Everyone likes colour (and it makes the arrow standout more. # +# -------------------------------------------------------------------------------- # + +red=$(tput setaf 1) +reset=$(tput sgr0) +bold=$(tput bold) + +# -------------------------------------------------------------------------------- # +# The main worker which is called when an error happens. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC2155 +function failure() +{ + # + # Remove the traps to ensure they dont trigger twice + # + trap '' ERR EXIT + + # + # Local variables to store the error code and signal that caused the failure + # + local code=$1 + local signal=$2 + + # + # Regex to parse the error log + # + local regex="^(.*): line ([[:digit:]]+): (.*)" + + # + # What file/line caused the error? + # + # shellcheck disable=SC2312 + IFS=' ' read -ra PARTS <<< "$(caller)" + lineno=${PARTS[0]} + filename=${PARTS[1]} + + # + # Only do something if there was an error code + # + if [[ ${code} != 0 ]]; then + # + # Exit doesnt mean no error just no ERR (divider by zero for example does this) + # + if [[ "${signal}" == 'EXIT' ]]; then + # + # Check for any stderr output + # + if [[ -f ${ERROR_FILE} ]]; then + mapfile -t errors < "${ERROR_FILE}" + rm -f "${ERROR_FILE}" + + # + # Loop over the errors (could be multiple in one file as err wasnt called) + # + for i in "${errors[@]}" + do + if [[ ${i} =~ ${regex} ]]; then + # + # Only process if the pattern matches (generally: file: line NN: error message) + # + if [[ ${#BASH_REMATCH[@]} == 4 ]]; then + lineno=${BASH_REMATCH[2]} + error_msg=${BASH_REMATCH[3]} + + # + # Display the details for the error + # + printf "\n%s%sFailed in %s on line %s with the following error:\n%s%s\n" "${bold}" "${red}" "${filename##*/}" "${lineno}" "${error_msg}" "${reset}" + awk 'NR>L-4 && NR>> ":""), reset, $0 }' L="${lineno}" bold="${bold}" red="${red}" reset="${reset}" "${filename}" + fi + fi + done + # + # Nothing in stderr - just show the details without an error message + # + else + printf "\n%s%sFailed in %s on line %s%s\n" "${bold}" "${red}" "${filename##*/}" "${lineno}" "${reset}" + awk 'NR>L-4 && NR>> ":""), reset, $0 }' L="${lineno}" bold="${bold}" red="${red}" reset="${reset}" "${filename}" + fi + elif [[ "${signal}" == 'ERR' ]]; then + # + # Check forany stderr output + # + if [[ -f ${ERROR_FILE} ]]; then + mapfile -t errors < "${ERROR_FILE}" + rm -f "${ERROR_FILE}" + + # + # Show the details (without or without the errors depending on if we have them) + # + if [[ ${#errors[@]} -eq 0 ]]; then + printf "\n%s%sFailed in %s on line %s with no error message.%s\n" "${bold}" "${red}" "${filename##*/}" "${lineno}" "${reset}" + else + printf "\n%s%sFailed in %s on line %s with the following error:\n%s%s\n" "${bold}" "${red}" "${filename##*/}" "${lineno}" "${errors[@]}" "${reset}" + fi + else + printf "\n%s%sParent call failure in %s on line %s%s\n" "${bold}" "${red}" "${filename##*/}" "${lineno}" "${reset}" + fi + # + # Show the code snipper + # + awk 'NR>L-4 && NR>> ":""), reset, $0 }' L="${lineno}" bold="${bold}" red="${red}" reset="${reset}" "${filename}" + else + printf "\n%s%sUnhandled signal '%s'%s\n" "${bold}" "${red}" "${signal}" "${reset}" + fi + fi +} + +# -------------------------------------------------------------------------------- # +# Simple wrapper to allow multiple traps to be set and identified. # +# -------------------------------------------------------------------------------- # + +function trap_with_arg() +{ + func="$1"; + shift + + for sig ; do + # shellcheck disable=SC2064 + trap "${func} ${sig}" "${sig}" + done +} + +# -------------------------------------------------------------------------------- # +# Bait the traps. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC2016 +trap_with_arg 'failure ${?}' ERR EXIT + +# -------------------------------------------------------------------------------- # +# End of Script # +# -------------------------------------------------------------------------------- # +# This is the end - nothing more to see here. # +# -------------------------------------------------------------------------------- # diff --git a/tests/execute-stack/child-1.sh b/tests/execute-stack/child-1.sh new file mode 100755 index 0000000..cbf8326 --- /dev/null +++ b/tests/execute-stack/child-1.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Ensure that trapper can correctly identify and locate errors in child scripts. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC1091 +source ../../src/trapper.sh + +./child-2.sh + +# -------------------------------------------------------------------------------- # +# End of Script # +# -------------------------------------------------------------------------------- # +# This is the end - nothing more to see here. # +# -------------------------------------------------------------------------------- # diff --git a/tests/execute-stack/child-2.sh b/tests/execute-stack/child-2.sh new file mode 100755 index 0000000..ac1d40d --- /dev/null +++ b/tests/execute-stack/child-2.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Ensure that trapper can correctly identify and locate errors in child scripts. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC1091 +source ../../src/trapper.sh + +list_files() +{ + ls /root/ +} + +list_files diff --git a/tests/execute-stack/parent.sh b/tests/execute-stack/parent.sh new file mode 100755 index 0000000..2fcfa88 --- /dev/null +++ b/tests/execute-stack/parent.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Ensure that trapper can correctly identify and locate errors in child scripts. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC1091 +source ../../src/trapper.sh + +./child-1.sh + +# -------------------------------------------------------------------------------- # +# End of Script # +# -------------------------------------------------------------------------------- # +# This is the end - nothing more to see here. # +# -------------------------------------------------------------------------------- # diff --git a/tests/source-stack/child-1.sh b/tests/source-stack/child-1.sh new file mode 100755 index 0000000..22a6a20 --- /dev/null +++ b/tests/source-stack/child-1.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Ensure that trapper can correctly identify and locate errors in sourced scripts. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC1091 +source ./child-2.sh + +# -------------------------------------------------------------------------------- # +# End of Script # +# -------------------------------------------------------------------------------- # +# This is the end - nothing more to see here. # +# -------------------------------------------------------------------------------- # diff --git a/tests/source-stack/child-2.sh b/tests/source-stack/child-2.sh new file mode 100755 index 0000000..e74f9cd --- /dev/null +++ b/tests/source-stack/child-2.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Ensure that trapper can correctly identify and locate errors in sourced scripts. # +# -------------------------------------------------------------------------------- # + +list_files() +{ + ls /root/ +} + +list_files + +# -------------------------------------------------------------------------------- # +# End of Script # +# -------------------------------------------------------------------------------- # +# This is the end - nothing more to see here. # +# -------------------------------------------------------------------------------- # diff --git a/tests/source-stack/parent.sh b/tests/source-stack/parent.sh new file mode 100755 index 0000000..2a40ffc --- /dev/null +++ b/tests/source-stack/parent.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Ensure that trapper can correctly identify and locate errors in sourced scripts. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC1091 +source ../../src/trapper.sh + +source ./child-1.sh + +# -------------------------------------------------------------------------------- # +# End of Script # +# -------------------------------------------------------------------------------- # +# This is the end - nothing more to see here. # +# -------------------------------------------------------------------------------- # diff --git a/tests/unbounded/unbounded.sh b/tests/unbounded/unbounded.sh new file mode 100755 index 0000000..9d4a795 --- /dev/null +++ b/tests/unbounded/unbounded.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# -------------------------------------------------------------------------------- # +# Description # +# -------------------------------------------------------------------------------- # +# Ensure that trapper can correctly identify and locate an unbounded variable. # +# -------------------------------------------------------------------------------- # + +# shellcheck disable=SC1091 +source ../../src/trapper.sh + +# shellcheck disable=SC2154 +echo "${FRED}" + +# -------------------------------------------------------------------------------- # +# End of Script # +# -------------------------------------------------------------------------------- # +# This is the end - nothing more to see here. # +# -------------------------------------------------------------------------------- #