diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 00000000..0146a1ce --- /dev/null +++ b/.bundle/config @@ -0,0 +1,7 @@ +--- +BUNDLE_BIN: "bin" +BUNDLE_PATH: "vendor/gems" +BUNDLE_CACHE_PATH: "vendor/cache" +BUNDLE_CACHE_ALL: "true" +BUNDLE_SPECIFIC_PLATFORM: "true" +BUNDLE_NO_INSTALL: "true" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..56e05dfe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +docs/ +bin/ +README.md +.gitignore +tmp/ +.git +coverage/ +vendor/gems/ +.github/ +.devcontainer/ +.ruby-lsp/ +docker-compose.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..93109362 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @GrantBirki diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..d6482214 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,98 @@ +# Copilot Instructions + +You are an AI assistant that specializes in software development for the Ruby programming language. + +## Environment Setup + +Regarding scripts that manage the environment or start the app, follow the guidance given by GitHub in their [Scripts to Rule Them All](https://github.blog/engineering/scripts-to-rule-them-all/) blog post. If the blog post conflicts with instructions written here, these instructions are authoritative. For example: + +Bootstrap the Ruby project by running: + +```bash +script/bootstrap +``` + +## Testing + +Ensure all unit tests pass by running the following: + +```bash +script/test +``` + +This project **requires 100% test coverage** of code, not including: + +- dependencies or their bin scripts +- tests +- scripts in `script/` +- contents of directories that begin with a dot (`.`) + +Tests are powered by Ruby's `rspec`. By running `script/test`, the tool `simplecov` will be automatically used and will exit with a non-zero code if the coverage is below 100%. + +## Linting + +Ensure the linter passes by running: + +```bash +script/lint +``` + +The linter is powered by `rubocop` with its config file located at `.rubocop.yml`. The linter will exit with a non-zero code if any issues are found. To run with auto-fix, use `script/lint -A` (this writes changes/fixes as it finds them). + +## Project Guidelines + +- Follow: + - Object-Oriented best practices, especially abstraction and encapsulation + - GRASP Principles, especially Information Expert, Creator, Indirection, Low Coupling, High Cohesion, and Pure Fabrication + - SOLID principles, especially Dependency Inversion, Open/Closed, and Single Responsibility + - Design Patterns defined by the Gang of Four, especially Abstract Factory, Factory Method, Chain of Responsibility, Command, Mediator, Observer, State, and Adaptor patterns. + - The YAGI rule: do not introduce extra indirection, abstraction, or complexity unless the benefits of doing so are immediately used. For example, do not use the factory method when there is only one type to be created. +- Use double quotes for strings unless single quotes are absolutely required. +- Base new work on latest `main` branch. +- Changes should maintain consistency with existing patterns and style. +- Document changes clearly and thoroughly, including updates to existing comments when appropriate. Try to use the same "voice" as the other comments, mimicking their tone and style. +- When responding to code refactoring suggestions, function suggestions, or other code changes, please keep your responses as concise as possible. We are capable engineers and can understand the code changes without excessive explanation. If you feel that a more detailed explanation is necessary, you can provide it, but keep it concise. +- When suggesting code changes, always opt for the most maintainable approach. Try your best to keep the code clean and follow DRY principles. Avoid unnecessary complexity and always consider the long-term maintainability of the code. +- When writing unit tests, try to consider edge cases as well as the main path of success. This will help ensure that the code is robust and can handle unexpected inputs or situations. +- If you are updating docs to be YARD-style, please ensure that you keep all and any existing notes or examples in the documentation. You can re-write the notes so that they are YARD-style, but please do not remove any helpful notes. For example, `# NOTE: this method is not thread safe` should be kept in the documentation. +- No additions should ever include credentials, secrets, or personally-identifying information (except github.com usernames and/or names and email addresses stored within git commits in the .git directory). +- Hard-coded strings should almost always be constant variables. +- In general, avoid introducing new dependencies. Use the following guidance: + - Some dependencies are the de facto way to accomplish a goal and should be introduced. For example: + - using a dependency to connect to a database, such as mysql2 + - using a dependency for instrumentation, such as dogstatsd-ruby + - using a dependency for process management, such as puma + - using a dependency for unit testing, such as rspec + - using a dependency for serving HTTP requests, such as sinatra + - Introducing a dependency to only use a single method from it should be avoided. Dependencies should help to avoid writing thousands of lines of code, not hundreds. + - Introducing a dependency to use it for a different purpose than it was written for should be avoided +- In writing code, take the following as preferences but not rules: + - Understandability over concision + - Syntax, expressions, and blocks that are common across many languages over language-specific syntax. + - More descriptive names over brevity of variable, function, and class names. + - The use of whitespace (newlines) over compactness of files. + - Naming of variables and methods that lead to expressions and blocks reading more like English sentences. + - Less lines of code over more. Keep changes minimal and focused. + +## Pull Request Requirements + +- All tests must pass. +- The linter must pass. +- Documentation must be up-to-date. +- Any new dependencies must be vendored. +- All new code must have YARD-style documentation. +- The body of the Pull Request should: + - Contain a summary of the changes. + - Make special note of any changes to dependencies. + - Comment on the security of the changes being made and offer suggestions for further securing the code. + +## Repository Organization + +- `.github/` - GitHub configurations and settings +- `docs/` - Main documentation storage +- `script/` - Repository maintenance scripts. Includes things like `script/bootstrap`, `script/test`, and `script/lint`. +- `config/` - Configuration files for the project. +- `lib/` - Main code for the project. This is where the main application/service code lives. +- `spec/` - Tests for the project. This is where the unit tests and acceptance tests live. +- `vendor/cache` - Vendored dependencies (Ruby Gems). +- `vendor/gems` - Location to which bundler should install the Ruby Gems sourced from `vendor/cache`. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a20a89b3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +--- +version: 2 +updates: + - package-ecosystem: bundler + vendor: true + directory: "/" + schedule: + interval: weekly + day: "monday" + time: "21:00" + groups: + prod-ruby-dependencies: + dependency-type: "production" + patterns: + - "*" + dev-ruby-dependencies: + dependency-type: "development" + patterns: + - "*" + - package-ecosystem: github-actions + directory: "/" + groups: + github-actions: + patterns: + - "*" + schedule: + interval: weekly + day: "tuesday" + time: "21:00" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..b3006dc3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: build + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + build: + name: build + + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: checkout + uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # pin@v1.244.0 + with: + bundler-cache: true + + - name: bootstrap + run: script/bootstrap + + - name: build + run: script/build diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..4a2a7e55 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,22 @@ +name: "Copilot Setup Steps" + +# Allows you to test the setup steps from your repository's "Actions" tab +on: workflow_dispatch + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + # Set the permissions to the lowest permissions possible needed for *your steps*. Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + steps: + - name: checkout + uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # pin@v1.244.0 + with: + bundler-cache: true + + - name: bootstrap + run: script/bootstrap diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..da0cfe29 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: lint + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # pin@v1.244.0 + with: + bundler-cache: true + + - name: bootstrap + run: script/bootstrap + + - name: lint + run: script/lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b4f3afcb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,156 @@ +name: release + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - lib/hooks/version.rb + +permissions: {} + +jobs: + build: + if: github.repository == 'github/hooks' + permissions: + contents: read + runs-on: ubuntu-latest + outputs: + artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} + gem_name: ${{ steps.build.outputs.gem_name }} + gem_version: ${{ steps.build.outputs.gem_version }} + gem_path: ${{ steps.build.outputs.gem_path }} + + steps: + - name: checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # pin@v1.244.0 + with: + bundler-cache: false + + - name: bootstrap + run: script/bootstrap + + # IMPORTANT: this step MUST export for the following outputs: + # gem_name: the name of the gem - ex: "my-cool-gem" + # gem_version: the version of the gem - ex: "1.0.0" + # gem_path: the path/filename of the gem - ex: "my-cool-gem-1.0.0.gem" + - name: build + id: build + run: script/build + + - name: upload artifact + uses: actions/upload-artifact@4.6.2 + id: upload-artifact + with: + path: "${{ steps.build.outputs.gem_path }}" + + release: + needs: build + environment: release + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + artifact-ids: ${{ needs.build.outputs.artifact-id }} + + - name: Publish to GitHub Packages + env: + OWNER: ${{ github.repository_owner }} + GEM_NAME: ${{ needs.build.outputs.gem_name }} + GEM_VERSION: ${{ needs.build.outputs.gem_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_PATH: "artifact" + run: | + GEM_HOST_API_KEY=${GITHUB_TOKEN} gem push --key github --host https://rubygems.pkg.github.com/${OWNER} $ARTIFACT_PATH/${GEM_NAME}-${GEM_VERSION}.gem + + - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # pin@v1.244.0 + with: + bundler-cache: false + + - name: bootstrap + run: script/bootstrap + + - name: Configure RubyGems Credentials + uses: rubygems/configure-rubygems-credentials@e3f5097339179e0d4c7321ab44209e7e02446746 # pin@main + + - name: sign ruby gem + env: + GEM_NAME: ${{ needs.build.outputs.gem_name }} + GEM_VERSION: ${{ needs.build.outputs.gem_version }} + ARTIFACT_PATH: "artifact" + run: bundle exec sigstore-cli sign ${ARTIFACT_PATH}/${GEM_NAME}-${GEM_VERSION}.gem --bundle ${GEM_NAME}-${GEM_VERSION}.sigstore.json + + - name: Publish to RubyGems + env: + GEM_NAME: ${{ needs.build.outputs.gem_name }} + GEM_VERSION: ${{ needs.build.outputs.gem_version }} + ARTIFACT_PATH: "artifact" + run: gem push ${ARTIFACT_PATH}/${GEM_NAME}-${GEM_VERSION}.gem --attestation ${GEM_NAME}-${GEM_VERSION}.sigstore.json + + - name: await gem + env: + GEM_NAME: ${{ needs.build.outputs.gem_name }} + GEM_VERSION: ${{ needs.build.outputs.gem_version }} + run: bundle exec rubygems-await "${GEM_NAME}:${GEM_VERSION}" --timeout 120 + + - name: GitHub Release + env: + GEM_NAME: ${{ needs.build.outputs.gem_name }} + GEM_VERSION: ${{ needs.build.outputs.gem_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_PATH: "artifact" + run: | + gh release create "v${GEM_VERSION}" \ + "${ARTIFACT_PATH}/${GEM_NAME}-${GEM_VERSION}.gem" \ + "${GEM_NAME}-${GEM_VERSION}.sigstore.json" \ + --title "v${GEM_VERSION}" \ + --generate-notes + + sign: + needs: [build, release] + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + contents: read + + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + artifact-ids: ${{ needs.build.outputs.artifact-id }} + + - name: attest build provenance + uses: actions/attest-build-provenance@v2.3.0 + with: + subject-path: "artifact/${{ needs.build.outputs.gem_path }}" + + verify: + permissions: {} + needs: [build, release, sign] + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + artifact-ids: ${{ needs.build.outputs.artifact-id }} + + - name: verify + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + ARTIFACT_PATH: "artifact/${{ needs.build.outputs.gem_path }}" + run: gh attestation verify "$ARTIFACT_PATH" --repo ${OWNER}/${REPO} --signer-workflow ${OWNER}/${REPO}/.github/workflows/release.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..0ff6b030 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + name: test + runs-on: ubuntu-latest + strategy: + matrix: + ruby: [ '3.1.2', '3.1.4', '3.2.2', '3.2.3', '3.3.0', '3.3.1', '3.4.0', '3.4.2', '3.4.3', '3.4.4' ] + + steps: + - name: checkout + uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # pin@v1.244.0 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + + - name: bootstrap + run: script/bootstrap + + - name: test + run: script/test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..82edbbeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +bin/ +coverage/ +logs/ +tmp/ +tarballs/ +vendor/gems/ +.idea +.byebug_history +.local/ +.DS_Store +.lesshst +*.pem +*.key +*.crt +*.csr +*.secret +hooks-ruby-*.gem diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..485bb5e3 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,16 @@ +inherit_gem: + rubocop-github: + - config/default.yml + +AllCops: + NewCops: enable + SuggestExtensions: false + DisplayCopNames: true + TargetRubyVersion: 3.4 + Exclude: + - "bin/**/*" + - "tmp/**/*" + - "vendor/**/*" + +Style/HashSyntax: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..f9892605 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.4 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..86d5fcd8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +ARG RUBY_VERSION=3 +FROM ruby:${RUBY_VERSION}-slim AS base + +# create a nonroot user +RUN useradd -m nonroot + +WORKDIR /app + +# install system dependencies +RUN apt-get -qq update && apt-get --no-install-recommends install -y \ + build-essential \ + git && \ + rm -rf /var/lib/apt/lists/* + +# set the BUNDLE_APP_CONFIG environment variable +ENV BUNDLE_APP_CONFIG=/app/.bundle + +# copy bundler config +COPY --chown=nonroot:nonroot .bundle ./.bundle + +# install core scripts +COPY --chown=nonroot:nonroot script ./script + +# copy core ruby files first +COPY --chown=nonroot:nonroot .ruby-version Gemfile Gemfile.lock ./ + +# copy vendored gems +COPY --chown=nonroot:nonroot vendor ./vendor + +# bootstrap the ruby environment +RUN RUBY_ENV=production script/bootstrap + +# copy the rest of the application +COPY --chown=nonroot:nonroot . . + +# change ownership of /app directory to nonroot user +RUN chown -R nonroot:nonroot /app + +# switch to the nonroot user +USER nonroot + +# set the environment to production +ENV RUBY_ENV=production diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..31ea7972 --- /dev/null +++ b/Gemfile @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +group :development do + gem "irb", "~> 1" + gem "rspec", "~> 3" + gem "rubocop", "~> 1" + gem "rubocop-github", "~> 0.26" + gem "rubocop-performance", "~> 1" + gem "rubocop-rspec", "~> 3" + gem "simplecov", "~> 0.22" + gem "simplecov-erb", "~> 1" + gem "vcr", "~> 6.3", ">= 6.3.1" + gem "webmock", "~> 3" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..434fa066 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,213 @@ +PATH + remote: . + specs: + hooks-ruby (0.0.1) + dry-schema (~> 1.14, >= 1.14.1) + grape (~> 2.3) + grape-swagger (~> 2.1, >= 2.1.2) + redacting-logger (~> 1.5) + retryable (~> 3.0, >= 3.0.5) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.0.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) + crack (1.0.0) + bigdecimal + rexml + date (3.4.1) + diff-lcs (1.6.2) + docile (1.4.1) + drb (2.2.3) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.14.1) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) + zeitwerk (~> 2.6) + dry-types (1.8.3) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + erb (5.0.1) + grape (2.3.0) + activesupport (>= 6) + dry-types (>= 1.1) + mustermann-grape (~> 1.1.0) + rack (>= 2) + zeitwerk + grape-swagger (2.1.2) + grape (>= 1.7, < 3.0) + rack-test (~> 2) + hashdiff (1.2.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + io-console (0.8.0) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.12.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + minitest (5.25.5) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) + mustermann-grape (1.1.0) + mustermann (>= 1.0.0) + parallel (1.27.0) + parser (3.3.8.0) + ast (~> 2.4.1) + racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + prism (1.4.0) + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + racc (1.8.1) + rack (3.1.16) + rack-test (2.2.0) + rack (>= 1.3) + rainbow (3.1.1) + rdoc (6.14.0) + erb + psych (>= 4.0.0) + redacting-logger (1.5.0) + logger (~> 1.6) + regexp_parser (2.10.0) + reline (0.6.1) + io-console (~> 0.5) + retryable (3.0.5) + rexml (3.4.1) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.4) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) + rubocop (1.76.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.45.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.45.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-github (0.26.0) + rubocop (>= 1.76) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.23) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.32.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rspec (3.6.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-erb (1.0.1) + simplecov (< 1.0) + simplecov-html (0.13.1) + simplecov_json_formatter (0.1.4) + stringio (3.1.7) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.3) + vcr (6.3.1) + base64 + webmock (3.25.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + zeitwerk (2.7.3) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + hooks-ruby! + irb (~> 1) + rspec (~> 3) + rubocop (~> 1) + rubocop-github (~> 0.26) + rubocop-performance (~> 1) + rubocop-rspec (~> 3) + simplecov (~> 0.22) + simplecov-erb (~> 1) + vcr (~> 6.3, >= 6.3.1) + webmock (~> 3) + +BUNDLED WITH + 2.6.7 diff --git a/README.md b/README.md index 26a6c7b4..24656694 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # hooks + +[![build](https://github.com/github/hooks/actions/workflows/build.yml/badge.svg)](https://github.com/github/hooks/actions/workflows/build.yml) +[![test](https://github.com/github/hooks/actions/workflows/test.yml/badge.svg)](https://github.com/github/hooks/actions/workflows/test.yml) +[![lint](https://github.com/github/hooks/actions/workflows/lint.yml/badge.svg)](https://github.com/github/hooks/actions/workflows/lint.yml) + A Pluggable Webhook Server Framework written in Ruby diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 00000000..dff2ff06 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,637 @@ +# `hooks` โ€” A Pluggable Ruby Webhook Server Framework + +## ๐Ÿ“œ 1. Project Overview + +`hooks` is a **pure-Ruby**, **Grape-based**, **Rack-compatible** webhook server gem that: + +* Dynamically mounts endpoints from per-team configs under a configurable `root_path` +* Loads **team handlers** and **global plugins** at boot +* Validates configs via **Dry::Schema**, failing fast on invalid YAML/JSON/Hash +* Supports **signature validation** (default HMAC) and **custom validator** classes +* Enforces **request limits** (body size) and **timeouts**, configurable at runtime +* Emits **basic metrics events** for downstream integration +* Ships with operational endpoints: + + * **GET** ``: liveness/readiness payload + * **GET** ``: JSON array of recent events + * **GET** ``: current gem version + +* Boots a demo `/hello` route when no config is supplied, to verify setup + +> **Server Agnostic:** `hooks` exports a Rack-compatible app. Mount under any Rack server (Puma, Unicorn, Thin, etc.). + +Note: The `hooks` gem name is already taken on RubyGems, so this project is named `hooks-ruby` there. + +--- + +## ๐ŸŽฏ 2. Core Goals + +1. **Config-Driven Endpoints** + + * Single file per endpoint: YAML, JSON, or Ruby Hash + * Merged into `AppConfig` at boot, validated + * Each endpoint `path` is prefixed by global `root_path` (default `/webhooks`) + +2. **Plugin Architecture** + + * **Team Handlers**: `class MyHandler < Hooks::Handlers::Base` + * Must implement `#call(payload:, headers:, config:)` method + * `payload`: parsed request body (JSON Hash or raw String) + * `headers`: HTTP headers as Hash with string keys + * `config`: merged endpoint configuration including `opts` section + * **Global Plugins**: `class MyPlugin < Hooks::Plugins::Lifecycle` + * Hook methods: `#on_request`, `#on_response`, `#on_error` + * **Signature Validators**: implement class method `.valid?(payload:, headers:, secret:, config:)` + * Return `true`/`false` for signature validation + * Access to full request context for custom validation logic + +3. **Security & Isolation** + + * Default JSON error responses, with detailed hooks + +4. **Operational Endpoints** + + * **Health**: liveness/readiness, config checksums + * **Metrics**: JSON events log (last N entries) + * **Version**: gem version report + +5. **Developer & Operator Experience** + + * Single entrypoint: `app = Hooks.build(...)` + * Multiple configuration methods: path(s), ENV, Ruby Hash + * Graceful shutdown on SIGINT/SIGTERM + * Structured JSON logging with `request_id`, `path`, `handler`, timestamp + * Scaffold generators for handlers and plugins + +--- + +## โš™๏ธ 3. Installation & Invocation + +### Gemfile + +```ruby +gem "hooks-ruby" +``` + +### Programmatic Invocation + +```ruby +require "hooks-ruby" + +# Returns a Rack-compatible app +app = Hooks.build( + config: "/path/to/config.yaml", # YAML, JSON, or Hash + log: MyCustomLogger.new, # Optional logger (must respond to #info, #error, etc.) + request_limit: 1_048_576, # Default max body size (bytes) + request_timeout: 15, # Default timeout (seconds) + root_path: "/webhooks" # Default mount prefix +) +``` + +Mount in `config.ru`: + +```ruby +run app +``` + +### ENV-Based Bootstrap + +Core configuration options can be provided via environment variables: + +```bash +# Core configuration +export HOOKS_CONFIG=./config/config.yaml + +# Runtime settings (override config file) +export HOOKS_REQUEST_LIMIT=1048576 +export HOOKS_REQUEST_TIMEOUT=15 +export HOOKS_GRACEFUL_SHUTDOWN_TIMEOUT=30 +export HOOKS_ROOT_PATH="/webhooks" + +# Logging +export HOOKS_LOG_LEVEL=info + +# Paths +export HOOKS_HANDLER_DIR=./handlers +export HOOKS_HEALTH_PATH=/health +export HOOKS_METRICS_PATH=/metrics +export HOOKS_VERSION_PATH=/version + +# Start the application +ruby -r hooks-ruby -e "run Hooks.build" +``` + +> **Hello-World Mode** +> If invoked without `config`, serves `GET /hello`: +> +> ```json +> { "message": "Hooks is working!" } +> ``` + +--- + +## ๐Ÿ“ 4. Directory Layout + +```text +lib/hooks/ +โ”œโ”€โ”€ app/ +โ”‚ โ”œโ”€โ”€ api.rb # Grape::API subclass exporting all endpoints +โ”‚ โ”œโ”€โ”€ router_builder.rb # Reads AppConfig to define routes +โ”‚ โ””โ”€โ”€ endpoint_builder.rb # Wraps each route: auth, signature, hooks, handler +โ”‚ +โ”œโ”€โ”€ core/ +โ”‚ โ”œโ”€โ”€ builder.rb # Hooks.build: config loading, validation, signal handling - builds a rack compatible app +โ”‚ โ”œโ”€โ”€ config_loader.rb # Loads + merges per-endpoint configs +โ”‚ โ”œโ”€โ”€ config_validator.rb # Dry::Schema-based validation +โ”‚ โ”œโ”€โ”€ logger_factory.rb # Structured JSON logger + context enrichment +โ”‚ โ”œโ”€โ”€ metrics_emitter.rb # Event emitter for request metrics +โ”‚ โ””โ”€โ”€ signal_handler.rb # Trap SIGINT/SIGTERM for graceful shutdown +โ”‚ +โ”œโ”€โ”€ handlers/ +โ”‚ โ””โ”€โ”€ base.rb # `Hooks::Handlers::Base` interface: defines #call +โ”‚ +โ”œโ”€โ”€ plugins/ +โ”‚ โ”œโ”€โ”€ lifecycle.rb # `Hooks::Plugins::Lifecycle` hooks (on_request, response, error) +โ”‚ โ””โ”€โ”€ signature_validator/ # Default & sample validators +โ”‚ โ”œโ”€โ”€ base.rb # Abstract interface +โ”‚ โ””โ”€โ”€ hmac_sha256.rb # Default implementation +โ”‚ +โ”œโ”€โ”€ version.rb # Provides `Hooks::VERSION` +โ””โ”€โ”€ hooks.rb # `require 'hooks'` entrypoint defining Hooks module +``` + +--- + +## ๐Ÿ› ๏ธ 5. Config Models + +### 5.1 Endpoint Config (per-file) + +```yaml +# config/endpoints/team1.yaml +path: /team1 # Mounted at /team1 +handler: Team1Handler # Class in handler_dir + +# Signature validation +verify_signature: + type: default # 'default' uses HMACSHA256, or a custom class name + secret_env_key: TEAM1_SECRET + header: X-Hub-Signature + algorithm: sha256 + +opts: # Freeform user-defined options + env: staging + teams: ["infra","billing"] +``` + +### 5.2 Global Config File + +```yaml +# config/config.yaml +handler_dir: ./handlers # handler class directory +log_level: info # debug | info | warn | error + +# Request handling +request_limit: 1048576 # max request body size (bytes) +request_timeout: 15 # seconds to allow per request + +# Path configuration +root_path: /webhooks # base path for all endpoint routes +health_path: /health # operational health endpoint +metrics_path: /metrics # operational metrics endpoint +version_path: /version # gem version endpoint + +# Runtime behavior +environment: production # development | production +endpoints_dir: ./config/endpoints # directory containing endpoint configs +``` + +--- + +## ๐Ÿ” 6. Core Components & Flow + +1. **Builder (`core/builder.rb`)** + + * Load config (env or file) via `config_loader` + * Load endpoint configs via `config_loader` + * Validate via `config_validator` (Dry::Schema); halt if invalid at boot + * Initialize structured JSON logger via `logger_factory` + * Emit startup `:request_start` for `/health`, `/metrics`, `/version` + * Trap SIGINT/SIGTERM for graceful shutdown + * Build and return Rack app from `app/api.rb` + +2. **API Definition (`app/api.rb`)** + + * Uses Grape::API + * Mounts: + + * `/hello` (demo) + * ``, ``, `` + * Each team endpoint under `/` + +3. **Router & Endpoint Builder** + + * For each endpoint config: + + * Define Grape route with: + + * **Before**: enforce `request_limit`, `request_timeout` + * **Signature**: call custom or default validator + * **Hooks**: run `on_request` plugins + * **Handler**: invoke `MyHandler.new.call(payload:, headers:, config:)` + * **After**: run `on_response` plugins + * **Rescue**: on exception, run `on_error`, rethrow or format JSON error + +4. **Metrics Emitter** + + * Listen to lifecycle events, build in-memory ring buffer of last N events + * `/metrics` returns the JSON array of these events (configurable size) + +5. **Graceful Shutdown** + + * On SIGINT/SIGTERM: allow in-flight requests to finish, exit + +--- + +## ๐Ÿ”’ 7. Security & Isolation + +* **Request Validation**: size, timeout, signature enforced systematically +* **Error Handling**: exceptions bubble to Grape catchall, with JSON schema + +--- + +## ๐Ÿšจ 8. Error Handling & Logging + +### Error Response Format + +**Default JSON Error Response:** + +```json +{ + "error": "Error message", + "code": 500, + "request_id": "uuid-string" +} +``` + +**Environment-specific behavior:** + +* **Development Mode**: includes full stack trace in `backtrace` field +* **Production Mode**: hides sensitive details, logs full context internally + +### Custom Error Handling + +Users can customize error responses via global plugins: + +```ruby +class CustomErrorPlugin < Hooks::Plugins::Lifecycle + def on_error(exception, env) + # Custom error processing, logging, or response formatting + { + error: "Custom error message", + code: determine_error_code(exception), + timestamp: Time.now.iso8601 + } + end +end +``` + +### Structured Logging + +Each log entry includes standardized fields: + +* `timestamp` (ISO8601) +* `level` (debug, info, warn, error) +* `message` +* `request_id` (UUID for request correlation) +* `path` (endpoint path) +* `handler` (handler class name) +* `status` (HTTP status code) +* `duration_ms` (request processing time) +* `user_agent`, `remote_ip` (when available) + +--- + +## ๐Ÿ“ˆ 9. Metrics & Instrumentation + +Simple request logging for basic observability: + +* Basic request/response logging with timestamps +* Simple error tracking +* Basic health check endpoint returning service status + +**Example log output:** + +```json +{ + "timestamp": "2025-06-09T10:30:00Z", + "level": "info", + "message": "Request processed", + "request_id": "uuid-string", + "path": "/webhooks/team1", + "handler": "Team1Handler", + "status": 200, + "duration_ms": 45 +} +``` + +--- + +## โšก 10. Configuration Loading & Precedence + +Configuration is loaded and merged in the following priority order (highest to lowest): + +1. **Programmatic parameters** passed to `Hooks.build(...)` +2. **Environment variables** (`HOOKS_*`) +3. **Config file** (YAML/JSON) +4. **Built-in defaults** + +**Example:** + +```ruby +# This programmatic setting will override ENV and file settings +app = Hooks.build( + request_timeout: 30, # Overrides HOOKS_REQUEST_TIMEOUT and config.yaml + config: "./config/config.yaml" +) +``` + +**Handler & Plugin Discovery:** + +* Handler classes are auto-discovered from `handler_dir` using file naming convention +* File `team1_handler.rb` โ†’ class `Team1Handler` +* Plugin classes are loaded from `plugin_dir` and registered based on class inheritance +* All classes must inherit from appropriate base classes to be recognized + +--- + +## ๐Ÿ› ๏ธ 11. CLI & Scaffolding + +Command-line interface via `bin/hooks`: + +```bash +# Create a new handler skeleton +hooks scaffold handler my_endpoint + +# Create a new global plugin skeleton +hooks scaffold plugin my_plugin + +# Validate existing configuration +hooks validate + +# Show current configuration summary +hooks config +``` + +**Generated Files:** + +* `handlers/my_endpoint_handler.rb` - Handler class skeleton +* `config/endpoints/my_endpoint.yaml` - Endpoint configuration template +* `plugins/my_plugin.rb` - Plugin class skeleton with lifecycle hooks + +--- + +## ๐Ÿ–ฅ๏ธ 16. CLI Utility: `hooks serve` + +The project provides a `hooks serve` command-line utility for running the webhook server directly, similar to `rails server`. + +### Usage + +```bash +hooks serve [options] +``` + +#### Common Options + +* `-p`, `--port PORT` โ€” Port to listen on (default: 3000) +* `-b`, `--bind HOST` โ€” Bind address (default: 0.0.0.0) +* `-e`, `--env ENV` โ€” Environment (default: production) +* `-c`, `--config PATH` โ€” Path to config file (YAML/JSON) +* `--no-puma` โ€” (Advanced) Use the default Rack handler instead of Puma +* `-h`, `--help` โ€” Show help message + +### Example + +```bash +hooks serve -p 8080 -c ./config/config.yaml +``` + +### How it Works + +* The CLI loads configuration from CLI args, ENV, or defaults. +* It builds the Rack app using `Hooks.build(...)`. +* By default, it starts the server using Puma (via `Rack::Handler::Puma`). +* If Puma is not available, it falls back to the default Rack handler (e.g., WEBrick), but Puma is strongly recommended and included as a dependency. + +### Implementation Sketch + +```ruby +# bin/hooks (excerpt) +require "hooks-ruby" +require "optparse" + +options = { + port: ENV.fetch("PORT", 3000), + bind: ENV.fetch("BIND", "0.0.0.0"), + env: ENV.fetch("RACK_ENV", "production"), + config: ENV["HOOKS_CONFIG"] || "./config/config.yaml", + use_puma: true +} + +OptionParser.new do |opts| + opts.banner = "Usage: hooks serve [options]" + opts.on("-pPORT", "--port=PORT", Integer, "Port to listen on") { |v| options[:port] = v } + opts.on("-bHOST", "--bind=HOST", String, "Bind address") { |v| options[:bind] = v } + opts.on("-eENV", "--env=ENV", String, "Environment") { |v| options[:env] = v } + opts.on("-cPATH", "--config=PATH", String, "Config file (YAML/JSON)") { |v| options[:config] = v } + opts.on("--no-puma", "Use default Rack handler instead of Puma") { options[:use_puma] = false } + opts.on("-h", "--help", "Show help") { puts opts; exit } +end.parse!(ARGV) + +app = Hooks.build(config: options[:config]) + +if options[:use_puma] + require "rack/handler/puma" + Rack::Handler::Puma.run(app, Host: options[:bind], Port: options[:port], environment: options[:env]) +else + Rack::Handler.default.run(app, Host: options[:bind], Port: options[:port]) +end +``` + +### Notes + +* Puma is included as a runtime dependency and is the default server for all environments. +* The CLI is suitable for both development and production use. +* All configuration options can be set via CLI flags, ENV variables, or config files. +* The CLI prints a startup banner with the version, port, and loaded endpoints. + +--- + +## ๐Ÿ“ฆ 13. Hello-World Default + +When no configuration is provided, the framework serves a demo endpoint for verification: + +**Endpoint:** `GET /hello` (default: `/webhooks/hello`) + +**Response:** + +```json +{ + "message": "Hooks is working!", + "version": "1.0.0", + "timestamp": "2025-06-09T10:30:00Z" +} +``` + +This allows immediate verification that the framework is properly installed and running. + +--- + +## ๐Ÿš€ 14. Production Deployment + +### Docker Support + +```dockerfile +FROM ruby:3.2-alpine +WORKDIR /app +COPY Gemfile* ./ +RUN bundle install --deployment --without development test +COPY . . +EXPOSE 3000 +CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] +``` + +### Health Check Integration + +The health endpoint provides comprehensive status information for load balancers and monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2025-06-09T10:30:00Z", + "version": "1.0.0", + "config_checksum": "abc123def456", + "endpoints_loaded": 5, + "plugins_loaded": 3, + "uptime_seconds": 3600 +} +``` + +### Performance Considerations + +* **Thread Safety**: All core components are thread-safe for multi-threaded servers +* **Memory Management**: Configurable metrics buffer prevents unbounded memory growth +* **Graceful Degradation**: Framework continues operating even if individual handlers fail + +### Security Best Practices + +* Use strong secrets for signature validation +* Monitor and alert on unusual request patterns via metrics +* Keep handler code minimal and well-tested + +--- + +## ๐Ÿ“š 15. API Reference + +### Core Classes + +#### `Hooks::Handlers::Base` + +Base class for all webhook handlers. + +```ruby +class MyHandler < Hooks::Handlers::Base + # @param payload [Hash, String] Parsed request body or raw string + # @param headers [Hash] HTTP headers + # @param config [Hash] Merged endpoint configuration + # @return [Hash, String, nil] Response body (auto-converted to JSON) + def call(payload:, headers:, config:) + # Handler implementation + { status: "processed", id: generate_id } + end +end +``` + +#### `Hooks::Plugins::Lifecycle` + +Base class for global plugins with lifecycle hooks. + +```ruby +class MyPlugin < Hooks::Plugins::Lifecycle + # Called before handler execution + # @param env [Hash] Rack environment + def on_request(env) + # Pre-processing logic + end + + # Called after successful handler execution + # @param env [Hash] Rack environment + # @param response [Hash] Handler response + def on_response(env, response) + # Post-processing logic + end + + # Called when any error occurs + # @param exception [Exception] The raised exception + # @param env [Hash] Rack environment + def on_error(exception, env) + # Error handling logic + end +end +``` + +#### `Hooks::Plugins::SignatureValidator::Base` + +Abstract base for custom signature validators. + +```ruby +class CustomValidator < Hooks::Plugins::SignatureValidator::Base + # @param payload [String] Raw request body + # @param headers [Hash] HTTP headers + # @param secret [String] Secret key for validation + # @param config [Hash] Endpoint configuration + # @return [Boolean] true if signature is valid + def self.valid?(payload:, headers:, secret:, config:) + # Custom validation logic + computed_signature = generate_signature(payload, secret) + provided_signature = headers[config[:header]] + secure_compare(computed_signature, provided_signature) + end +end +``` + +--- + +## ๐Ÿ”’ HMAC Signature Validation Example + +A typical HMAC validation in a handler or middleware might look like: + +> This example shows how to implement HMAC signature validation in a handler or middleware for a sinatra-based app which is kinda close to Grape but not quite. It should be used as a reference as this is from GitHub's official docs on how to validate their webhooks. + +```ruby +def verify_signature(payload_body) + signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body) + return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE_256']) +end +``` + +This ensures the payload is authentic and untampered, using a shared secret and the SHA256 algorithm. + +### Configuration Schema + +Complete schema for endpoint configurations: + +```yaml +# Required fields +path: string # Endpoint path (mounted under root_path) +handler: string # Handler class name + +# Optional signature validation +verify_signature: + type: string # 'default' or custom validator class name + secret_env_key: string # ENV key containing secret + header: string # Header containing signature (default: X-Hub-Signature) + algorithm: string # Hash algorithm (default: sha256) + +# Optional user-defined data +opts: hash # Arbitrary configuration data +``` diff --git a/hooks.gemspec b/hooks.gemspec new file mode 100644 index 00000000..ccbb0ddd --- /dev/null +++ b/hooks.gemspec @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative "lib/hooks/version" + +Gem::Specification.new do |spec| + spec.name = "hooks-ruby" + spec.version = Hooks::VERSION + spec.authors = ["github", "GrantBirki"] + spec.license = "MIT" + + spec.summary = "A Pluggable Webhook Server Framework written in Ruby" + spec.description = <<~SPEC_DESC + A Pluggable Webhook Server Framework written in Ruby + SPEC_DESC + + spec.homepage = "https://github.com/github/hooks" + spec.metadata = { + "bug_tracker_uri" => "https://github.com/github/hooks/issues" + } + + spec.add_dependency "redacting-logger", "~> 1.5" + spec.add_dependency "retryable", "~> 3.0", ">= 3.0.5" + spec.add_dependency "dry-schema", "~> 1.14", ">= 1.14.1" + spec.add_dependency "grape", "~> 2.3" + spec.add_dependency "grape-swagger", "~> 2.1", ">= 2.1.2" + + spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0") + + spec.files = %w[LICENSE README.md hooks.gemspec] + spec.files += Dir.glob("lib/**/*.rb") + spec.require_paths = ["lib"] +end diff --git a/lib/hooks/version.rb b/lib/hooks/version.rb new file mode 100644 index 00000000..ff9ddb80 --- /dev/null +++ b/lib/hooks/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Hooks + VERSION = "0.0.1" +end diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 00000000..2b9fd5af --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,16 @@ +#! /usr/bin/env bash + +set -e # prevent any kind of script failures + +source script/env "$@" + +# bootstrap gem dependencies +if [ "$BUNDLE_WITHOUT" == "" ]; then + echo -e "${BLUE}Installing Gems for ${PURPLE}development${BLUE}...${OFF}" +else + echo -e "${BLUE}Installing Gems for ${GREEN}production${BLUE}...${OFF}" +fi + +# what gets installed is set via the BUNDLE_WITHOUT env var in the script/env file +bundle install --local +bundle binstubs --all diff --git a/script/build b/script/build new file mode 100755 index 00000000..83b3bf05 --- /dev/null +++ b/script/build @@ -0,0 +1,19 @@ +#! /usr/bin/env bash + +set -e + +source script/env "$@" + +GEMSPEC_NAME="$(basename *.gemspec .gemspec)" +GEM_NAME=$(ruby -e "spec = Gem::Specification.load('$GEMSPEC_NAME.gemspec'); puts spec.name") +GEM_VERSION=$(ruby -e "spec = Gem::Specification.load('$GEMSPEC_NAME.gemspec'); puts spec.version") + +gem build $GEMSPEC_NAME.gemspec + +if [[ "$CI" == "true" ]]; then + echo "gem_name=$GEM_NAME" >> $GITHUB_OUTPUT + echo "gem_version=$GEM_VERSION" >> $GITHUB_OUTPUT + echo "gem_path=$GEM_NAME-$GEM_VERSION.gem" >> $GITHUB_OUTPUT +fi + +echo -e "๐Ÿ“ฆ ${GREEN}successfully${OFF} built ${PURPLE}$GEM_NAME-$GEM_VERSION.gem${OFF}" diff --git a/script/env b/script/env new file mode 100755 index 00000000..c992258b --- /dev/null +++ b/script/env @@ -0,0 +1,60 @@ +#! /usr/bin/env bash + +set -e # prevent any kind of script failures + +# COLORS +export OFF='\033[0m' +export RED='\033[0;31m' +export GREEN='\033[0;32m' +export BLUE='\033[0;34m' +export PURPLE='\033[0;35m' + +# set RUBY_ENV to development (as a default) if not set +: "${RUBY_ENV:=development}" + +export RUBY_ENV + +# set the working directory to the root of the project +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" +export DIR + +# The name of the repository is the name of the directory (usually) +REPO_NAME=$(basename "$PWD") +export REPO_NAME + +TARBALL_DIR="$DIR/tarballs" +export TARBALL_DIR + +# set the ruby version to the one specified in the .ruby-version file +[ -z "$RBENV_VERSION" ] && RBENV_VERSION=$(cat "$DIR/.ruby-version") +export RBENV_VERSION + +# set the path to include the rbenv shims if they exist +[ -d "/usr/share/rbenv/shims" ] && export PATH=/usr/share/rbenv/shims:$PATH + +# detect OS version and architecture +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + PLATFORM="linux" + VERSION=$(grep '^VERSION_CODENAME=' /etc/os-release | cut -d '=' -f2 || echo "unknown") +elif [[ "$OSTYPE" == "darwin"* ]]; then + PLATFORM="macos" + VERSION=$(sw_vers -productVersion || echo "unknown") +else + PLATFORM="unknown" + VERSION="unknown" +fi + +ARCH=$(uname -m || echo "unknown") + +export PLATFORM +export VERSION +export ARCH + +shopt -s nocasematch # enable case-insensitive matching +if [[ "$RUBY_ENV" == "production" ]]; then + export BUNDLE_WITHOUT="development" +fi +shopt -u nocasematch # disable case-insensitive matching + +# make the vendor/cache directory if it doesn't exist +mkdir -p "$DIR/vendor/cache" diff --git a/script/lint b/script/lint new file mode 100755 index 00000000..79fab871 --- /dev/null +++ b/script/lint @@ -0,0 +1,10 @@ +#! /usr/bin/env bash + +set -e + +source script/env "$@" + +# run linter +echo -e "\n๐Ÿค– ${BLUE}Running Rubocop: $(date "+%H:%M:%S")${OFF}\n" + +bundle exec rubocop -c .rubocop.yml "$@" diff --git a/script/server b/script/server new file mode 100755 index 00000000..b3e3dbf6 --- /dev/null +++ b/script/server @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +set -e + +source script/env "$@" + +bundle exec ruby lib/cli.rb "$@" diff --git a/script/test b/script/test new file mode 100755 index 00000000..aa4589f3 --- /dev/null +++ b/script/test @@ -0,0 +1,43 @@ +#! /usr/bin/env bash + +set -e + +source script/env "$@" + +# run tests +echo -e "\n๐Ÿงช ${BLUE}Running tests: $(date "+%H:%M:%S")${OFF}\n" + +bundle exec bin/rspec spec && rspec_exit=$? || rspec_exit=$? + +total_coverage=$(cat "$DIR/coverage/total-coverage.txt") + +if grep -q "100.0" "$DIR/coverage/total-coverage.txt"; then + cov_exit=0 + echo -e "\nโœ… Total Coverage: ${GREEN}$total_coverage${OFF}" +else + cov_exit=1 + echo -e "\nโŒ Total Coverage: ${RED}$total_coverage${OFF}" +fi + +echo "" +echo "---------------------------------------" +echo "๐Ÿ“Š Summary Results" +echo "---------------------------------------" +echo "" + +if [[ $rspec_exit == 0 ]]; then + echo -e "โœ… ${GREEN}rspec: exitcode=${rspec_exit}${OFF}" +else + echo -e "โŒ ${RED}rspec: exitcode=${rspec_exit}${OFF}" +fi + +if [[ $cov_exit == 0 ]]; then + echo -e "โœ… ${GREEN}coverage: exitcode=${cov_exit}${OFF}" +else + echo -e "โŒ ${RED}coverage: exitcode=${cov_exit}${OFF}" +fi + +[ "$rspec_exit" -gt 0 ] && exit 1 +[ $cov_exit -gt 0 ] && exit 1 + +exit 0 diff --git a/spec/hooks_spec.rb b/spec/hooks_spec.rb new file mode 100644 index 00000000..6246d153 --- /dev/null +++ b/spec/hooks_spec.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "spec_helper" diff --git a/spec/lib/hooks/version_spec.rb b/spec/lib/hooks/version_spec.rb new file mode 100644 index 00000000..fdebeca8 --- /dev/null +++ b/spec/lib/hooks/version_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +describe Hooks::VERSION do + it "has a version number" do + expect(Hooks::VERSION).not_to be nil + expect(Hooks::VERSION).to match(/^\d+\.\d+\.\d+$/) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..a808337d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "simplecov" +require "simplecov-erb" +require "rspec" +require "time" + +TIME_MOCK = "2025-01-01T00:00:00Z" + +COV_DIR = File.expand_path("../coverage", File.dirname(__FILE__)) + +SimpleCov.root File.expand_path("..", File.dirname(__FILE__)) +SimpleCov.coverage_dir COV_DIR + +SimpleCov.minimum_coverage 100 + +SimpleCov.at_exit do + File.write("#{COV_DIR}/total-coverage.txt", SimpleCov.result.covered_percent) + SimpleCov.result.format! +end + +SimpleCov.start do + add_filter "config/" + add_filter "spec/" + add_filter "vendor/gems/" +end + +# Require all Ruby files in the lib directory +Dir.glob(File.expand_path("../lib/**/*.rb", __dir__)).each do |file| + require file +end + +RSpec.configure do |config| + config.before(:each) do + fake_time = Time.parse(TIME_MOCK) + allow(Time).to receive(:now).and_return(fake_time) + allow(Time).to receive(:new).and_return(fake_time) + allow(Time).to receive(:iso8601).and_return(fake_time) + allow(fake_time).to receive(:utc).and_return(fake_time) + allow(fake_time).to receive(:iso8601).and_return(TIME_MOCK) + + allow(Kernel).to receive(:sleep) + allow_any_instance_of(Kernel).to receive(:sleep) + allow_any_instance_of(Object).to receive(:sleep) + end +end diff --git a/vendor/cache/activesupport-8.0.2.gem b/vendor/cache/activesupport-8.0.2.gem new file mode 100644 index 00000000..24b49354 Binary files /dev/null and b/vendor/cache/activesupport-8.0.2.gem differ diff --git a/vendor/cache/addressable-2.8.7.gem b/vendor/cache/addressable-2.8.7.gem new file mode 100644 index 00000000..c4890680 Binary files /dev/null and b/vendor/cache/addressable-2.8.7.gem differ diff --git a/vendor/cache/ast-2.4.3.gem b/vendor/cache/ast-2.4.3.gem new file mode 100644 index 00000000..1f5e5c25 Binary files /dev/null and b/vendor/cache/ast-2.4.3.gem differ diff --git a/vendor/cache/base64-0.3.0.gem b/vendor/cache/base64-0.3.0.gem new file mode 100644 index 00000000..12f53f14 Binary files /dev/null and b/vendor/cache/base64-0.3.0.gem differ diff --git a/vendor/cache/benchmark-0.4.1.gem b/vendor/cache/benchmark-0.4.1.gem new file mode 100644 index 00000000..90cd2725 Binary files /dev/null and b/vendor/cache/benchmark-0.4.1.gem differ diff --git a/vendor/cache/bigdecimal-3.2.2.gem b/vendor/cache/bigdecimal-3.2.2.gem new file mode 100644 index 00000000..ed8d2e43 Binary files /dev/null and b/vendor/cache/bigdecimal-3.2.2.gem differ diff --git a/vendor/cache/concurrent-ruby-1.3.5.gem b/vendor/cache/concurrent-ruby-1.3.5.gem new file mode 100644 index 00000000..1cd9f527 Binary files /dev/null and b/vendor/cache/concurrent-ruby-1.3.5.gem differ diff --git a/vendor/cache/connection_pool-2.5.3.gem b/vendor/cache/connection_pool-2.5.3.gem new file mode 100644 index 00000000..23c398fc Binary files /dev/null and b/vendor/cache/connection_pool-2.5.3.gem differ diff --git a/vendor/cache/crack-1.0.0.gem b/vendor/cache/crack-1.0.0.gem new file mode 100644 index 00000000..e3cbaf45 Binary files /dev/null and b/vendor/cache/crack-1.0.0.gem differ diff --git a/vendor/cache/date-3.4.1.gem b/vendor/cache/date-3.4.1.gem new file mode 100644 index 00000000..fe7bd0ad Binary files /dev/null and b/vendor/cache/date-3.4.1.gem differ diff --git a/vendor/cache/diff-lcs-1.6.2.gem b/vendor/cache/diff-lcs-1.6.2.gem new file mode 100644 index 00000000..21c4c77c Binary files /dev/null and b/vendor/cache/diff-lcs-1.6.2.gem differ diff --git a/vendor/cache/docile-1.4.1.gem b/vendor/cache/docile-1.4.1.gem new file mode 100644 index 00000000..b292f4e5 Binary files /dev/null and b/vendor/cache/docile-1.4.1.gem differ diff --git a/vendor/cache/drb-2.2.3.gem b/vendor/cache/drb-2.2.3.gem new file mode 100644 index 00000000..0c78b283 Binary files /dev/null and b/vendor/cache/drb-2.2.3.gem differ diff --git a/vendor/cache/dry-configurable-1.3.0.gem b/vendor/cache/dry-configurable-1.3.0.gem new file mode 100644 index 00000000..11af638b Binary files /dev/null and b/vendor/cache/dry-configurable-1.3.0.gem differ diff --git a/vendor/cache/dry-core-1.1.0.gem b/vendor/cache/dry-core-1.1.0.gem new file mode 100644 index 00000000..33e0c5ba Binary files /dev/null and b/vendor/cache/dry-core-1.1.0.gem differ diff --git a/vendor/cache/dry-inflector-1.2.0.gem b/vendor/cache/dry-inflector-1.2.0.gem new file mode 100644 index 00000000..627f7f25 Binary files /dev/null and b/vendor/cache/dry-inflector-1.2.0.gem differ diff --git a/vendor/cache/dry-initializer-3.2.0.gem b/vendor/cache/dry-initializer-3.2.0.gem new file mode 100644 index 00000000..31d3c6f0 Binary files /dev/null and b/vendor/cache/dry-initializer-3.2.0.gem differ diff --git a/vendor/cache/dry-logic-1.6.0.gem b/vendor/cache/dry-logic-1.6.0.gem new file mode 100644 index 00000000..83dec7cc Binary files /dev/null and b/vendor/cache/dry-logic-1.6.0.gem differ diff --git a/vendor/cache/dry-schema-1.14.1.gem b/vendor/cache/dry-schema-1.14.1.gem new file mode 100644 index 00000000..fe0c923b Binary files /dev/null and b/vendor/cache/dry-schema-1.14.1.gem differ diff --git a/vendor/cache/dry-types-1.8.3.gem b/vendor/cache/dry-types-1.8.3.gem new file mode 100644 index 00000000..77973654 Binary files /dev/null and b/vendor/cache/dry-types-1.8.3.gem differ diff --git a/vendor/cache/erb-5.0.1.gem b/vendor/cache/erb-5.0.1.gem new file mode 100644 index 00000000..d0902424 Binary files /dev/null and b/vendor/cache/erb-5.0.1.gem differ diff --git a/vendor/cache/grape-2.3.0.gem b/vendor/cache/grape-2.3.0.gem new file mode 100644 index 00000000..c75b33b4 Binary files /dev/null and b/vendor/cache/grape-2.3.0.gem differ diff --git a/vendor/cache/grape-swagger-2.1.2.gem b/vendor/cache/grape-swagger-2.1.2.gem new file mode 100644 index 00000000..0d39c0fa Binary files /dev/null and b/vendor/cache/grape-swagger-2.1.2.gem differ diff --git a/vendor/cache/hashdiff-1.2.0.gem b/vendor/cache/hashdiff-1.2.0.gem new file mode 100644 index 00000000..1359998a Binary files /dev/null and b/vendor/cache/hashdiff-1.2.0.gem differ diff --git a/vendor/cache/i18n-1.14.7.gem b/vendor/cache/i18n-1.14.7.gem new file mode 100644 index 00000000..9307337f Binary files /dev/null and b/vendor/cache/i18n-1.14.7.gem differ diff --git a/vendor/cache/io-console-0.8.0.gem b/vendor/cache/io-console-0.8.0.gem new file mode 100644 index 00000000..7a39c003 Binary files /dev/null and b/vendor/cache/io-console-0.8.0.gem differ diff --git a/vendor/cache/irb-1.15.2.gem b/vendor/cache/irb-1.15.2.gem new file mode 100644 index 00000000..1d053448 Binary files /dev/null and b/vendor/cache/irb-1.15.2.gem differ diff --git a/vendor/cache/json-2.12.2.gem b/vendor/cache/json-2.12.2.gem new file mode 100644 index 00000000..71389d8d Binary files /dev/null and b/vendor/cache/json-2.12.2.gem differ diff --git a/vendor/cache/language_server-protocol-3.17.0.5.gem b/vendor/cache/language_server-protocol-3.17.0.5.gem new file mode 100644 index 00000000..40a28d80 Binary files /dev/null and b/vendor/cache/language_server-protocol-3.17.0.5.gem differ diff --git a/vendor/cache/lint_roller-1.1.0.gem b/vendor/cache/lint_roller-1.1.0.gem new file mode 100644 index 00000000..0f874b6d Binary files /dev/null and b/vendor/cache/lint_roller-1.1.0.gem differ diff --git a/vendor/cache/logger-1.7.0.gem b/vendor/cache/logger-1.7.0.gem new file mode 100644 index 00000000..061f1ccc Binary files /dev/null and b/vendor/cache/logger-1.7.0.gem differ diff --git a/vendor/cache/minitest-5.25.5.gem b/vendor/cache/minitest-5.25.5.gem new file mode 100644 index 00000000..2ffec491 Binary files /dev/null and b/vendor/cache/minitest-5.25.5.gem differ diff --git a/vendor/cache/mustermann-3.0.3.gem b/vendor/cache/mustermann-3.0.3.gem new file mode 100644 index 00000000..ec4d1260 Binary files /dev/null and b/vendor/cache/mustermann-3.0.3.gem differ diff --git a/vendor/cache/mustermann-grape-1.1.0.gem b/vendor/cache/mustermann-grape-1.1.0.gem new file mode 100644 index 00000000..480f83bb Binary files /dev/null and b/vendor/cache/mustermann-grape-1.1.0.gem differ diff --git a/vendor/cache/parallel-1.27.0.gem b/vendor/cache/parallel-1.27.0.gem new file mode 100644 index 00000000..1b86f818 Binary files /dev/null and b/vendor/cache/parallel-1.27.0.gem differ diff --git a/vendor/cache/parser-3.3.8.0.gem b/vendor/cache/parser-3.3.8.0.gem new file mode 100644 index 00000000..4571f816 Binary files /dev/null and b/vendor/cache/parser-3.3.8.0.gem differ diff --git a/vendor/cache/pp-0.6.2.gem b/vendor/cache/pp-0.6.2.gem new file mode 100644 index 00000000..25704968 Binary files /dev/null and b/vendor/cache/pp-0.6.2.gem differ diff --git a/vendor/cache/prettyprint-0.2.0.gem b/vendor/cache/prettyprint-0.2.0.gem new file mode 100644 index 00000000..0944aaba Binary files /dev/null and b/vendor/cache/prettyprint-0.2.0.gem differ diff --git a/vendor/cache/prism-1.4.0.gem b/vendor/cache/prism-1.4.0.gem new file mode 100644 index 00000000..005bf8ed Binary files /dev/null and b/vendor/cache/prism-1.4.0.gem differ diff --git a/vendor/cache/psych-5.2.6.gem b/vendor/cache/psych-5.2.6.gem new file mode 100644 index 00000000..becbf807 Binary files /dev/null and b/vendor/cache/psych-5.2.6.gem differ diff --git a/vendor/cache/public_suffix-6.0.2.gem b/vendor/cache/public_suffix-6.0.2.gem new file mode 100644 index 00000000..0baf25c6 Binary files /dev/null and b/vendor/cache/public_suffix-6.0.2.gem differ diff --git a/vendor/cache/racc-1.8.1.gem b/vendor/cache/racc-1.8.1.gem new file mode 100644 index 00000000..ad9e6bbd Binary files /dev/null and b/vendor/cache/racc-1.8.1.gem differ diff --git a/vendor/cache/rack-3.1.16.gem b/vendor/cache/rack-3.1.16.gem new file mode 100644 index 00000000..0a48c300 Binary files /dev/null and b/vendor/cache/rack-3.1.16.gem differ diff --git a/vendor/cache/rack-test-2.2.0.gem b/vendor/cache/rack-test-2.2.0.gem new file mode 100644 index 00000000..b0b9c9d8 Binary files /dev/null and b/vendor/cache/rack-test-2.2.0.gem differ diff --git a/vendor/cache/rainbow-3.1.1.gem b/vendor/cache/rainbow-3.1.1.gem new file mode 100644 index 00000000..863181a2 Binary files /dev/null and b/vendor/cache/rainbow-3.1.1.gem differ diff --git a/vendor/cache/rdoc-6.14.0.gem b/vendor/cache/rdoc-6.14.0.gem new file mode 100644 index 00000000..2dde547a Binary files /dev/null and b/vendor/cache/rdoc-6.14.0.gem differ diff --git a/vendor/cache/redacting-logger-1.5.0.gem b/vendor/cache/redacting-logger-1.5.0.gem new file mode 100644 index 00000000..dbd36217 Binary files /dev/null and b/vendor/cache/redacting-logger-1.5.0.gem differ diff --git a/vendor/cache/regexp_parser-2.10.0.gem b/vendor/cache/regexp_parser-2.10.0.gem new file mode 100644 index 00000000..63358cc5 Binary files /dev/null and b/vendor/cache/regexp_parser-2.10.0.gem differ diff --git a/vendor/cache/reline-0.6.1.gem b/vendor/cache/reline-0.6.1.gem new file mode 100644 index 00000000..98ae6be5 Binary files /dev/null and b/vendor/cache/reline-0.6.1.gem differ diff --git a/vendor/cache/retryable-3.0.5.gem b/vendor/cache/retryable-3.0.5.gem new file mode 100644 index 00000000..94561620 Binary files /dev/null and b/vendor/cache/retryable-3.0.5.gem differ diff --git a/vendor/cache/rexml-3.4.1.gem b/vendor/cache/rexml-3.4.1.gem new file mode 100644 index 00000000..b0c5c846 Binary files /dev/null and b/vendor/cache/rexml-3.4.1.gem differ diff --git a/vendor/cache/rspec-3.13.1.gem b/vendor/cache/rspec-3.13.1.gem new file mode 100644 index 00000000..74792105 Binary files /dev/null and b/vendor/cache/rspec-3.13.1.gem differ diff --git a/vendor/cache/rspec-core-3.13.4.gem b/vendor/cache/rspec-core-3.13.4.gem new file mode 100644 index 00000000..2a2780f9 Binary files /dev/null and b/vendor/cache/rspec-core-3.13.4.gem differ diff --git a/vendor/cache/rspec-expectations-3.13.5.gem b/vendor/cache/rspec-expectations-3.13.5.gem new file mode 100644 index 00000000..51409fdd Binary files /dev/null and b/vendor/cache/rspec-expectations-3.13.5.gem differ diff --git a/vendor/cache/rspec-mocks-3.13.5.gem b/vendor/cache/rspec-mocks-3.13.5.gem new file mode 100644 index 00000000..05da2b39 Binary files /dev/null and b/vendor/cache/rspec-mocks-3.13.5.gem differ diff --git a/vendor/cache/rspec-support-3.13.4.gem b/vendor/cache/rspec-support-3.13.4.gem new file mode 100644 index 00000000..0d49eada Binary files /dev/null and b/vendor/cache/rspec-support-3.13.4.gem differ diff --git a/vendor/cache/rubocop-1.76.1.gem b/vendor/cache/rubocop-1.76.1.gem new file mode 100644 index 00000000..024cb307 Binary files /dev/null and b/vendor/cache/rubocop-1.76.1.gem differ diff --git a/vendor/cache/rubocop-ast-1.45.1.gem b/vendor/cache/rubocop-ast-1.45.1.gem new file mode 100644 index 00000000..9e1a70a9 Binary files /dev/null and b/vendor/cache/rubocop-ast-1.45.1.gem differ diff --git a/vendor/cache/rubocop-github-0.26.0.gem b/vendor/cache/rubocop-github-0.26.0.gem new file mode 100644 index 00000000..dfd9b234 Binary files /dev/null and b/vendor/cache/rubocop-github-0.26.0.gem differ diff --git a/vendor/cache/rubocop-performance-1.25.0.gem b/vendor/cache/rubocop-performance-1.25.0.gem new file mode 100644 index 00000000..847b3f73 Binary files /dev/null and b/vendor/cache/rubocop-performance-1.25.0.gem differ diff --git a/vendor/cache/rubocop-rails-2.32.0.gem b/vendor/cache/rubocop-rails-2.32.0.gem new file mode 100644 index 00000000..257d7b7c Binary files /dev/null and b/vendor/cache/rubocop-rails-2.32.0.gem differ diff --git a/vendor/cache/rubocop-rspec-3.6.0.gem b/vendor/cache/rubocop-rspec-3.6.0.gem new file mode 100644 index 00000000..18b48e0e Binary files /dev/null and b/vendor/cache/rubocop-rspec-3.6.0.gem differ diff --git a/vendor/cache/ruby-progressbar-1.13.0.gem b/vendor/cache/ruby-progressbar-1.13.0.gem new file mode 100644 index 00000000..c50b94b2 Binary files /dev/null and b/vendor/cache/ruby-progressbar-1.13.0.gem differ diff --git a/vendor/cache/ruby2_keywords-0.0.5.gem b/vendor/cache/ruby2_keywords-0.0.5.gem new file mode 100644 index 00000000..d311c5d0 Binary files /dev/null and b/vendor/cache/ruby2_keywords-0.0.5.gem differ diff --git a/vendor/cache/securerandom-0.4.1.gem b/vendor/cache/securerandom-0.4.1.gem new file mode 100644 index 00000000..05072cab Binary files /dev/null and b/vendor/cache/securerandom-0.4.1.gem differ diff --git a/vendor/cache/simplecov-0.22.0.gem b/vendor/cache/simplecov-0.22.0.gem new file mode 100644 index 00000000..ce8f9794 Binary files /dev/null and b/vendor/cache/simplecov-0.22.0.gem differ diff --git a/vendor/cache/simplecov-erb-1.0.1.gem b/vendor/cache/simplecov-erb-1.0.1.gem new file mode 100644 index 00000000..4557e73f Binary files /dev/null and b/vendor/cache/simplecov-erb-1.0.1.gem differ diff --git a/vendor/cache/simplecov-html-0.13.1.gem b/vendor/cache/simplecov-html-0.13.1.gem new file mode 100644 index 00000000..17948593 Binary files /dev/null and b/vendor/cache/simplecov-html-0.13.1.gem differ diff --git a/vendor/cache/simplecov_json_formatter-0.1.4.gem b/vendor/cache/simplecov_json_formatter-0.1.4.gem new file mode 100644 index 00000000..75f6f6e0 Binary files /dev/null and b/vendor/cache/simplecov_json_formatter-0.1.4.gem differ diff --git a/vendor/cache/stringio-3.1.7.gem b/vendor/cache/stringio-3.1.7.gem new file mode 100644 index 00000000..bca0b39f Binary files /dev/null and b/vendor/cache/stringio-3.1.7.gem differ diff --git a/vendor/cache/tzinfo-2.0.6.gem b/vendor/cache/tzinfo-2.0.6.gem new file mode 100644 index 00000000..2c16da8a Binary files /dev/null and b/vendor/cache/tzinfo-2.0.6.gem differ diff --git a/vendor/cache/unicode-display_width-3.1.4.gem b/vendor/cache/unicode-display_width-3.1.4.gem new file mode 100644 index 00000000..7c2a1186 Binary files /dev/null and b/vendor/cache/unicode-display_width-3.1.4.gem differ diff --git a/vendor/cache/unicode-emoji-4.0.4.gem b/vendor/cache/unicode-emoji-4.0.4.gem new file mode 100644 index 00000000..bae638f0 Binary files /dev/null and b/vendor/cache/unicode-emoji-4.0.4.gem differ diff --git a/vendor/cache/uri-1.0.3.gem b/vendor/cache/uri-1.0.3.gem new file mode 100644 index 00000000..afd77ba9 Binary files /dev/null and b/vendor/cache/uri-1.0.3.gem differ diff --git a/vendor/cache/vcr-6.3.1.gem b/vendor/cache/vcr-6.3.1.gem new file mode 100644 index 00000000..7ca4c043 Binary files /dev/null and b/vendor/cache/vcr-6.3.1.gem differ diff --git a/vendor/cache/webmock-3.25.1.gem b/vendor/cache/webmock-3.25.1.gem new file mode 100644 index 00000000..9fcca204 Binary files /dev/null and b/vendor/cache/webmock-3.25.1.gem differ diff --git a/vendor/cache/zeitwerk-2.7.3.gem b/vendor/cache/zeitwerk-2.7.3.gem new file mode 100644 index 00000000..31cb70ca Binary files /dev/null and b/vendor/cache/zeitwerk-2.7.3.gem differ