diff --git a/.do/specs.ts b/.do/specs.ts new file mode 100644 index 000000000..4c984d7e6 --- /dev/null +++ b/.do/specs.ts @@ -0,0 +1,93 @@ +import type { + App_service_spec, + App_spec, + Apps_image_source_spec, + App_variable_definition, +} from "../.github/scripts/node_modules/@digitalocean/dots"; + +const DIGITALOCEAN_BASE_IMAGE: Apps_image_source_spec = { + registry: "tahminator", + registryType: "DOCKER_HUB", + repository: "codebloom", + // override tag + tag: "latest", +}; + +const DIGITALOCEAN_BASE_SERVICE: App_service_spec = { + name: "codebloom", + healthCheck: { + failureThreshold: 9, + httpPath: "/api", + periodSeconds: 10, + successThreshold: 1, + timeoutSeconds: 1, + }, + livenessHealthCheck: { + failureThreshold: 9, + httpPath: "/api", + periodSeconds: 10, + successThreshold: 1, + timeoutSeconds: 1, + }, + httpPort: 8080, + instanceCount: 1, + instanceSizeSlug: "apps-s-1vcpu-1gb-fixed", +}; + +const DIGITALOCEAN_BASE_SPEC: App_spec = { + region: "nyc", + ingress: { + rules: [ + { + component: { + name: "codebloom", + }, + match: { + path: { + prefix: "/", + }, + }, + }, + ], + }, +}; + +export function prodSpec(envs: App_variable_definition[]): App_spec { + return { + ...DIGITALOCEAN_BASE_SPEC, + name: "codebloom-prod", + services: [ + { + ...DIGITALOCEAN_BASE_SERVICE, + image: { + ...DIGITALOCEAN_BASE_IMAGE, + tag: "latest", + }, + envs, + }, + ], + }; +} + +export function stgSpec(envs: App_variable_definition[]): App_spec { + return { + ...DIGITALOCEAN_BASE_SPEC, + name: "codebloom-staging", + services: [ + { + ...DIGITALOCEAN_BASE_SERVICE, + image: { + ...DIGITALOCEAN_BASE_IMAGE, + tag: "staging-latest", + }, + envs, + }, + ], + domains: [ + { + domain: "stg.codebloom.patinanetwork.org", + type: "PRIMARY", + }, + ], + }; +} diff --git a/.env.ci b/.env.ci index c7ef32c4e..87d77dfc1 100644 Binary files a/.env.ci and b/.env.ci differ diff --git a/.github/composite/build-image/action.yml b/.github/composite/build-image/action.yml new file mode 100644 index 000000000..b39b13319 --- /dev/null +++ b/.github/composite/build-image/action.yml @@ -0,0 +1,56 @@ +name: "Build & Upload Docker Image" +description: "Build & (optionally) upload Docker Image to Docker Registry" + +inputs: + GPG_PRIVATE_KEY: + description: "GPG Private Key" + required: true + GPG_PASSPHRASE: + description: "GPG Passphrase" + required: true + DOCKER_UPLOAD: + description: "Boolean indicating whether the image should be uploaded to Docker registry or not." + required: false + default: true + TAG_PREFIX: + description: "Docker tags prefix" + required: false + SERVER_PROFILES: + description: "Profile(s) to apply to Codebloom instance." + required: false + default: prod + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ inputs.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ inputs.GPG_PASSPHRASE }} + + - name: Set up pnpm + uses: pnpm/action-setup@master + with: + version: 10.24.0 + cache: true + cache_dependency_path: js/pnpm-lock.yaml + package_json_file: js/package.json + + - name: Set up OpenJDK 25 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "25" + cache: "maven" + + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + + - name: Run script + shell: bash + run: bun .github/scripts/build-image + env: + DOCKER_UPLOAD: ${{ inputs.DOCKER_UPLOAD }} + TAG_PREFIX: ${{ inputs.TAG_PREFIX }} + SERVER_PROFILES: ${{ inputs.SERVER_PROFILES }} diff --git a/.github/composite/check-notion-commits/action.yml b/.github/composite/check-notion-commits/action.yml deleted file mode 100644 index 3b573178d..000000000 --- a/.github/composite/check-notion-commits/action.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: "Check Notion Commits" -description: 'Ensure all commits in the PR match the Notion Task ID (run after "Check Notion PR")' -inputs: - PR_ID: - description: "PR ID" - required: true - NOTION_ID: - description: "The Notion ID parsed from the PR title" - required: true - -runs: - using: "composite" - steps: - - name: Verify Commit Messages - id: verify - shell: bash - continue-on-error: true - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - EXPECTED="${{ inputs.NOTION_ID }}" - FAILED_COMMITS="" - - COMMITS=$(gh pr view "${{ inputs.PR_ID }}" --json commits --jq '.commits[].messageHeadline') - - while IFS= read -r MESSAGE; do - if [[ ! "$MESSAGE" == "$EXPECTED"* ]]; then - echo "::warning::Invalid commit: $MESSAGE" - FAILED_COMMITS+="$MESSAGE"$'\n' - fi - done <<< "$COMMITS" - - if [ -n "$FAILED_COMMITS" ]; then - # strip last \n - FAILED_COMMITS="${FAILED_COMMITS%$'\n'}" - - echo "failed_list<> "$GITHUB_OUTPUT" - echo "$FAILED_COMMITS" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - exit 1 - fi - - - name: Send PR Message on Failure - if: steps.verify.outcome == 'failure' - uses: ./.github/composite/send-message - with: - prId: ${{ inputs.PR_ID }} - message: | - ### Commit Validation Failed - The following commits do not start with the required Notion ID `${{ inputs.NOTION_ID }}`: - - ``` - ${{ steps.verify.outputs.failed_list }} - ``` - - Please rebase and update your commit messages. - All messages should be of the following format: `${{ inputs.NOTION_ID }}: Example commit` - - - name: Finalize Failure - if: steps.verify.outcome == 'failure' - shell: bash - run: | - echo "One or more commits do not match the Notion ID." - exit 1 diff --git a/.github/composite/check-notion-pr/action.yml b/.github/composite/check-notion-pr/action.yml deleted file mode 100644 index 101c2ed43..000000000 --- a/.github/composite/check-notion-pr/action.yml +++ /dev/null @@ -1,291 +0,0 @@ -name: "Check Notion PR" -description: "Run validation(s) on the given PR ID against Notion" - -inputs: - PR_ID: - description: "PR ID" - required: true - GPG_PRIVATE_KEY: - description: "GPG Private Key" - required: true - GPG_PASSPHRASE: - description: "GPG Passphrase" - required: true - -outputs: - notion_id: - description: "The parsed Notion ID" - value: ${{ steps.parse_id.outputs.id }} - context: - description: "The Notion task title and description combined" - value: ${{ steps.check_notion.outputs.context }} - -runs: - using: "composite" - steps: - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Load secrets - uses: ./.github/composite/load-secrets - with: - GPG_PRIVATE_KEY: ${{ inputs.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ inputs.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci - - - name: Parse the numerical string prefix from the PR title - id: parse_id - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - TITLE="$(gh pr view "${{ inputs.PR_ID }}" --json title -q .title)" - PREFIX="$(echo "$TITLE" | grep -oE '^-?[0-9]+' || true)" - - if [ -z "$PREFIX" ]; then - echo "No numeric prefix found in PR title: $TITLE" >&2 - echo "status=failed" >> "$GITHUB_OUTPUT" - exit 1 - fi - - echo "id=$PREFIX" >> "$GITHUB_OUTPUT" - - - name: Check if the Notion task ID exists - id: check_notion - shell: bash - run: | - set -euo pipefail - - ID="${{ steps.parse_id.outputs.id }}" - BODY='{ - "filter": { - "property": "ID", - "unique_id": { - "equals": '${ID}' - } - } - }' - - result=$(curl --request POST \ - --header "accept: application/json" \ - --header "Authorization: Bearer ${NOTION_SECRET}" \ - --header "Content-Type: application/json" \ - --header "Notion-Version: 2025-09-03" \ - --data "${BODY}" \ - https://api.notion.com/v1/data_sources/$NOTION_TASK_DB_ID/query \ - | jq '(.results[0] // null) | {id: .properties.ID.unique_id.number, public_url: .public_url, page_id: .id, title: ((.properties."Task name".title // []) | map(.plain_text) | join("")) }') - - if [ -z "$result" ] || [ "$result" = "null" ] || [ "$result" = "[]" ]; then - echo "found=false" >> "$GITHUB_OUTPUT" - echo "public_url=null" >> "$GITHUB_OUTPUT" - else - PARSED_ID=$(echo "$result" | jq -r '.id') - - if [ "$PARSED_ID" = "null" ]; then - # TODO: Replace this out, very hacky - PARSED_ID=-1 - fi; - - if [ "$PARSED_ID" -ne "$ID" ]; then - echo "found=false" >> "$GITHUB_OUTPUT" - echo "public_url=null" >> "$GITHUB_OUTPUT" - echo "notion_id=null" >> "$GITHUB_OUTPUT" - else - PUBLIC_URL=$(echo "$result" | jq -r '.public_url') - PAGE_ID=$(echo "$result" | jq -r '.page_id') - TITLE=$(echo "$result" | jq -r '.title // ""') - - fetch_blocks() { - local block_id=$1 - local indent=$2 - - local response=$(curl -s --request GET \ - --header "accept: application/json" \ - --header "Authorization: Bearer ${NOTION_SECRET}" \ - --header "Notion-Version: 2025-09-03" \ - "https://api.notion.com/v1/blocks/${block_id}/children") - - echo "$response" | jq -c '.results[]?' | while IFS= read -r block; do - echo "$block" | jq -r --arg indent "$indent" ' - if .type == "paragraph" and (.paragraph.rich_text? // null) != null then - $indent + ([.paragraph.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "bulleted_list_item" and (.bulleted_list_item.rich_text? // null) != null then - $indent + "- " + ([.bulleted_list_item.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "numbered_list_item" and (.numbered_list_item.rich_text? // null) != null then - $indent + "1. " + ([.numbered_list_item.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "heading_1" and (.heading_1.rich_text? // null) != null then - $indent + "# " + ([.heading_1.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "heading_2" and (.heading_2.rich_text? // null) != null then - $indent + "## " + ([.heading_2.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "heading_3" and (.heading_3.rich_text? // null) != null then - $indent + "### " + ([.heading_3.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "quote" and (.quote.rich_text? // null) != null then - $indent + "> " + ([.quote.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "to_do" and (.to_do.rich_text? // null) != null then - $indent + (if .to_do.checked then "[x] " else "[ ] " end) + ([.to_do.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "toggle" and (.toggle.rich_text? // null) != null then - $indent + ([.toggle.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "callout" and (.callout.rich_text? // null) != null then - $indent + ([.callout.rich_text[]? | .plain_text // ""] | join("")) - elif .type == "code" and (.code.rich_text? // null) != null then - $indent + "```" + (.code.language // "") + "\n" + ([.code.rich_text[]? | .plain_text // ""] | join("")) + "\n" + $indent + "```" - else - empty - end' - - local has_children=$(echo "$block" | jq -r '.has_children // false') - local child_block_id=$(echo "$block" | jq -r '.id') - - if [ "$has_children" = "true" ] && [ -n "$child_block_id" ]; then - fetch_blocks "$child_block_id" "$indent " - fi - done - } - - DESCRIPTION=$(fetch_blocks "$PAGE_ID" "") - - CONTEXT="${TITLE}\n${DESCRIPTION}" - - echo "found=true" >> "$GITHUB_OUTPUT" - echo "public_url=$PUBLIC_URL" >> "$GITHUB_OUTPUT" - echo "notion_id=$PARSED_ID" >> "$GITHUB_OUTPUT" - { - echo "context<> "$GITHUB_OUTPUT" - fi - fi - - - name: Send a message to the PR if task does not exist - if: steps.check_notion.outputs.found == 'false' - uses: ./.github/composite/send-message - with: - prId: ${{ inputs.PR_ID }} - message: | - Task ID ${{ steps.parse_id.outputs.id || 'N/A'}} does not exist on Notion. Please set a valid Notion task ID to the start of the PR title. - - - name: Send a message to the PR if title does not have number prefix - if: failure() && steps.parse_id.outcome == 'failure' - uses: ./.github/composite/send-message - with: - prId: ${{ inputs.PR_ID }} - message: | - ## TITLE ID REQUIRED - - Please add the ID of your Notion ticket to the title of the PR. - - **Example:** `420: Foo Bar` - - - name: Fail if task does not exist - if: steps.check_notion.outputs.found == 'false' - shell: bash - run: | - echo "Task verification failed." - exit 1 - - - name: Update PR description with public link - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - PUBLIC_URL=${{ steps.check_notion.outputs.public_url }} - ID=${{ steps.parse_id.outputs.id }} - PR_ID=${{ inputs.PR_ID }} - - if [[ "$PUBLIC_URL" = "null" ]]; then - echo "Public URL set to null when it should not be. Exiting" - exit 1 - fi - - CURRENT_BODY="$(gh pr view "$PR_ID" --json body -q .body)" - - NEW_LINE="## [$ID]($PUBLIC_URL)" - - NEW_BODY=$(echo -e "$CURRENT_BODY" | awk -v link="$NEW_LINE" ' - FNR == 2 { - print link - } - FNR != 2 { - print $0 - } - ') - - gh pr edit "$PR_ID" --body "$NEW_BODY" - - - name: Update Notion task with PR link - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - PR_LINK=${{ github.server_url }}/${{ github.repository }}/pull/${{ inputs.PR_ID }} - PR_ID="${{ inputs.PR_ID }}" - NOTION_ID="${{ steps.parse_id.outputs.id }}" - - DATA_BODY='{ - "filter": { - "property": "ID", - "unique_id": { - "equals": '${NOTION_ID}' - } - } - }' - - notion_data=$(curl --request POST \ - --header "accept: application/json" \ - --header "Authorization: Bearer ${NOTION_SECRET}" \ - --header "Content-Type: application/json" \ - --header "Notion-Version: 2025-09-03" \ - --data "${DATA_BODY}" \ - https://api.notion.com/v1/data_sources/$NOTION_TASK_DB_ID/query | jq '.') - - NOTION_PAGE_ID=$(echo "$notion_data" | jq -r '.results[0].id') - - if [ "$NOTION_PAGE_ID" == "null" ] || [ -z "$NOTION_PAGE_ID" ]; then - echo "Error: Could not find Notion page for ID $NOTION_ID. Exiting." - exit 1 - fi - - prs_raw=$(echo "$notion_data" | jq -r '.results[0].properties."PRs (AUTO)".rich_text') - existing_prs=$(echo "$prs_raw" | jq -r '.[].plain_text') - - if echo "$existing_prs" | grep -Fq "$PR_LINK"; then - echo "PR link $PR_LINK is already present in Notion task. No update needed." - exit 0 - fi - - new_pr_obj=$(printf '{"text": {"content": "%s\\n"}}' "$PR_LINK") - new_prs_arr=$(echo "$prs_raw" | jq --arg new_obj_str "$new_pr_obj" '. + [$new_obj_str | fromjson]') - - PATCH_BODY='{ - "properties": { - "PRs (AUTO)": { - "rich_text": '$new_prs_arr' - } - } - }' - - status_code_output=$(curl -s -w "HTTPSTATUS:%{http_code}" \ - --request PATCH \ - --header "Authorization: Bearer ${NOTION_SECRET}" \ - --header "Content-Type: application/json" \ - --header "Notion-Version: 2025-09-03" \ - --data "$PATCH_BODY" \ - "https://api.notion.com/v1/pages/$NOTION_PAGE_ID") - - status_code=$(echo "$status_code_output" | sed -E 's/.*HTTPSTATUS:([0-9]{3}).*/\1/') - response_body=$(echo "$status_code_output" | sed -E 's/HTTPSTATUS:[0-9]{3}//') - - if [[ "$status_code" -ne "200" ]]; then - echo "Failed to update Notion task. Status code was $status_code." - exit 1 - fi - - echo "Notion task updated successfully with PR link: $PR_LINK." diff --git a/.github/composite/disable-mandb/action.yml b/.github/composite/disable-mandb/action.yml deleted file mode 100644 index 75d93dc65..000000000 --- a/.github/composite/disable-mandb/action.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: "Disable man-db" -description: "Disable unnecessary man-db triggers (speed up CI time)" -runs: - using: "composite" - steps: - - name: Check GPG secret key & passphrase exists - shell: bash - run: sudo bash -ec 'echo "set man-db/auto-update false" | debconf-communicate; dpkg-reconfigure man-db' diff --git a/.github/composite/load-secrets/action.yml b/.github/composite/load-secrets/action.yml index 64392f6da..162822458 100644 --- a/.github/composite/load-secrets/action.yml +++ b/.github/composite/load-secrets/action.yml @@ -1,5 +1,6 @@ name: "Load Secrets" description: "Load secrets & mask all loaded secrets" + inputs: GPG_PRIVATE_KEY: description: "GPG Private Key" @@ -9,12 +10,10 @@ inputs: required: true UNLOAD_ENVIRONMENTS: description: "Comma separated string of what environment files to load" + runs: using: "composite" steps: - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - name: Check GPG secret key & passphrase exists shell: bash run: | @@ -37,8 +36,8 @@ runs: shell: bash run: git-crypt --version - - name: Run load secrets script + - name: Run script shell: bash - run: bash .github/scripts/load-secrets.sh + run: bun .github/scripts/load-secrets env: UNLOAD_ENVIRONMENTS: ${{ inputs.UNLOAD_ENVIRONMENTS }} diff --git a/.github/composite/notion-checks/action.yml b/.github/composite/notion-checks/action.yml new file mode 100644 index 000000000..3631ff64f --- /dev/null +++ b/.github/composite/notion-checks/action.yml @@ -0,0 +1,39 @@ +name: "Run Notion Checks" +description: "Run Notion validation(s) on the given PR ID" + +inputs: + PR_ID: + description: "PR ID" + required: true + GPG_PRIVATE_KEY: + description: "GPG Private Key" + required: true + GPG_PASSPHRASE: + description: "GPG Passphrase" + required: true + GET_GHA_OUTPUT: + description: "If true, will output an object with data that can be interfaced in GitHub Actions." + required: false + default: false + +outputs: + context: + description: "Notion task context for AI review" + value: ${{ steps.run_script.outputs.context }} + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - name: Run script + id: run_script + shell: bash + run: bun .github/scripts/notion + env: + PR_ID: ${{ inputs.PR_ID }} + GET_GHA_OUTPUT: ${{ inputs.GET_GHA_OUTPUT }} diff --git a/.github/composite/redeploy/action.yml b/.github/composite/redeploy/action.yml new file mode 100644 index 000000000..63e392579 --- /dev/null +++ b/.github/composite/redeploy/action.yml @@ -0,0 +1,54 @@ +name: "Re-Deploy to DigitalOcean" +description: "Trigger a deployment to DigitalOcean and migrate the associated database." + +inputs: + GPG_PRIVATE_KEY: + description: "GPG Private Key" + required: true + GPG_PASSPHRASE: + description: "GPG Passphrase" + required: true + ENVIRONMENT: + description: '"staging" or "production"' + required: false + default: production + SHA: + description: "(Staging only). Current commit SHA" + required: false + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - name: Set up pnpm + uses: pnpm/action-setup@master + with: + version: 10.24.0 + cache: true + cache_dependency_path: js/pnpm-lock.yaml + package_json_file: js/package.json + + - name: Set up OpenJDK 25 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "25" + cache: "maven" + + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + + - name: Run script + shell: bash + run: bun .github/scripts/redeploy + env: + DOCKER_UPLOAD: ${{ inputs.DOCKER_UPLOAD }} + TAG_PREFIX: ${{ inputs.TAG_PREFIX }} + SERVER_PROFILES: ${{ inputs.SERVER_PROFILES }} + ENVIRONMENT: ${{ inputs.ENVIRONMENT }} + SHA: ${{ inputs.SHA }} diff --git a/.github/composite/send-message/action.yml b/.github/composite/send-message/action.yml index 53d70a293..06859d09c 100644 --- a/.github/composite/send-message/action.yml +++ b/.github/composite/send-message/action.yml @@ -20,17 +20,10 @@ inputs: runs: using: "composite" steps: - - name: Post PR Message - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.token }} - script: | - const prId = ${{ inputs.prId }}; - const message = ${{ toJSON(inputs.message) }}; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(prId, 10), - body: message - }); + - name: Run script + shell: bash + run: bun .github/scripts/utils/send-message/gha + env: + PR_ID: ${{ inputs.prId }} + MESSAGE: ${{ inputs.message }} + GH_TOKEN: ${{ inputs.token }} diff --git a/.github/composite/setup-ci/action.yml b/.github/composite/setup-ci/action.yml new file mode 100644 index 000000000..2cd218dc6 --- /dev/null +++ b/.github/composite/setup-ci/action.yml @@ -0,0 +1,52 @@ +name: "Setup CI" +description: "Setup dependencies required to run CI (disable man-db)" + +inputs: + GPG_PASSPHRASE: + description: "GPG passphrase if secrets will be used." + required: false + GPG_PRIVATE_KEY: + description: "GPG private key if secrets will be used." + required: false + GITHUB_TOKEN: + description: "GitHub token used to send message" + required: false + default: ${{ github.token }} + +runs: + using: "composite" + steps: + - name: Export GitHub Token + shell: bash + run: echo "GH_TOKEN=${{ inputs.GITHUB_TOKEN }}" >> $GITHUB_ENV + + - name: Disable man-db + shell: bash + run: sudo bash -ec 'echo "set man-db/auto-update false" | debconf-communicate; dpkg-reconfigure man-db' + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('.github/scripts/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install deps + shell: bash + run: bun install --cwd .github/scripts --frozen-lockfile + + - name: Load GPG secret key + uses: crazy-max/ghaction-import-gpg@v6 + if: inputs.GPG_PRIVATE_KEY != '' && inputs.GPG_PASSPHRASE != '' + with: + gpg_private_key: ${{ inputs.GPG_PRIVATE_KEY }} + passphrase: ${{ inputs.GPG_PASSPHRASE }} + + - name: Install git-crypt + if: inputs.GPG_PRIVATE_KEY != '' && inputs.GPG_PASSPHRASE != '' + uses: flydiverny/setup-git-crypt@v4 diff --git a/.github/composite/test/backend-pre-test/action.yml b/.github/composite/test/backend-pre-test/action.yml index e58004745..45c9c39ee 100644 --- a/.github/composite/test/backend-pre-test/action.yml +++ b/.github/composite/test/backend-pre-test/action.yml @@ -3,8 +3,8 @@ description: "Run backend linter & formatter checks + attempt to compile (NO TES runs: using: "composite" steps: - - name: Disable man-db - uses: ./.github/composite/disable-mandb + - name: Setup CI + uses: ./.github/composite/setup-ci - name: Set up OpenJDK 25 uses: actions/setup-java@v4 @@ -13,13 +13,6 @@ runs: java-version: "25" cache: "maven" - - name: Verify Java version - shell: bash - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - name: Run script shell: bash - run: bash .github/scripts/run-backend-compile-tests.sh + run: bun .github/scripts/test/run-backend-compile-tests diff --git a/.github/composite/test/backend-test/action.yml b/.github/composite/test/backend-test/action.yml new file mode 100644 index 000000000..5b61971af --- /dev/null +++ b/.github/composite/test/backend-test/action.yml @@ -0,0 +1,47 @@ +name: "Backend test" +description: "Run backend tests" + +inputs: + GPG_PRIVATE_KEY: + description: "GPG Private Key" + required: true + GPG_PASSPHRASE: + description: "GPG Passphrase" + required: true + UPLOAD_TEST_COV: + description: "Boolean indicating whether tests should be uploaded to Codecov or not." + required: false + default: true + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ inputs.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ inputs.GPG_PASSPHRASE }} + + - name: Set up OpenJDK 25 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "25" + cache: "maven" + + - name: Run script + shell: bash + run: bun .github/scripts/test/run-backend-tests + + - name: Upload JaCoCo HTML report + uses: actions/upload-artifact@v4 + if: ${{ inputs.UPLOAD_TEST_COV == true }} + with: + name: jacoco-report + path: target/site/jacoco/ + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + if: ${{ inputs.UPLOAD_TEST_COV == true }} + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/composite/test/frontend-test/action.yml b/.github/composite/test/frontend-test/action.yml new file mode 100644 index 000000000..5559d3ee0 --- /dev/null +++ b/.github/composite/test/frontend-test/action.yml @@ -0,0 +1,38 @@ +name: "Frontend Test" +description: "Run frontend tests" + +input: + GPG_PRIVATE_KEY: + description: "GPG Private Key" + required: true + GPG_PASSPHRASE: + description: "GPG Passphrase" + required: true + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ inputs.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ inputs.GPG_PASSPHRASE }} + + - name: Set up pnpm + uses: pnpm/action-setup@master + with: + version: 10.24.0 + cache: true + cache_dependency_path: js/pnpm-lock.yaml + package_json_file: js/package.json + + - name: Set up OpenJDK 25 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "25" + cache: "maven" + + - name: Run script + shell: bash + run: bun .github/scripts/test/run-frontend-tests diff --git a/.github/composite/validate-db/action.yml b/.github/composite/validate-db/action.yml new file mode 100644 index 000000000..1998a0d68 --- /dev/null +++ b/.github/composite/validate-db/action.yml @@ -0,0 +1,36 @@ +name: "Validate DB" +description: "Validate the current db/ folder at a current commit against a database" + +inputs: + GPG_PRIVATE_KEY: + description: "GPG Private Key" + required: true + GPG_PASSPHRASE: + description: "GPG Passphrase" + required: true + ENVIRONMENT: + description: '"staging" or "production"' + required: false + default: production + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - name: Set up OpenJDK 25 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "25" + cache: "maven" + + - name: Run script + shell: bash + run: bun .github/scripts/validate-db + env: + ENVIRONMENT: ${{ inputs.ENVIRONMENT }} diff --git a/.github/scripts/.gitignore b/.github/scripts/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/.github/scripts/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.github/scripts/.prettierignore b/.github/scripts/.prettierignore new file mode 100644 index 000000000..ea7f33b0d --- /dev/null +++ b/.github/scripts/.prettierignore @@ -0,0 +1 @@ +bun.lock diff --git a/.github/scripts/.prettierrc b/.github/scripts/.prettierrc new file mode 100644 index 000000000..c70c4d8a1 --- /dev/null +++ b/.github/scripts/.prettierrc @@ -0,0 +1,4 @@ +{ + "experimentalTernaries": true, + "semi": true +} diff --git a/.github/scripts/build-image.sh b/.github/scripts/build-image.sh deleted file mode 100644 index cb99d7715..000000000 --- a/.github/scripts/build-image.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/local-db.sh" -source "$DIR/run-backend-instance.sh" -trap 'backend_cleanup; db_cleanup' EXIT - -export TZ="America/New_York" - -db_startup -backend_startup - -corepack enable pnpm -pnpm --dir js i -D --frozen-lockfile -pnpm --dir js run generate - -TIMESTAMP="$(date +%Y.%m.%d-%H.%M.%S)" -GIT_SHA="$(git rev-parse --short HEAD)" -TAG_PREFIX="${TAG_PREFIX:-}" -TAGS=( - "tahminator/codebloom:${TAG_PREFIX}latest" - "tahminator/codebloom:${TAG_PREFIX}${TIMESTAMP}" - "tahminator/codebloom:${TAG_PREFIX}${GIT_SHA}" -) - -echo "Building image with tags:" -printf ' - %s\n' "${TAGS[@]}" - -[ -n "${DOCKER_HUB_PAT:-}" ] && echo "DOCKER_HUB_PAT found" || echo "DOCKER_HUB_PAT missing or empty" - -echo "${DOCKER_HUB_PAT}" | docker login -u "tahminator" --password-stdin - -docker buildx create --use --name codebloom-builder || docker buildx use codebloom-builder - -if [[ "${DOCKER_UPLOAD:-true}" == "true" ]]; then - BUILD_MODE="--push" -else - BUILD_MODE="--load" -fi -SERVER_PROFILES="${SERVER_PROFILES:-prod}" - -if [[ "$SERVER_PROFILES" == "stg" ]]; then - VITE_STAGING_ARG="--build-arg VITE_STAGING=true" -else - VITE_STAGING_ARG="" -fi - -docker buildx build \ - $BUILD_MODE \ - --file infra/Dockerfile \ - --build-arg SERVER_PROFILES="$SERVER_PROFILES" \ - --build-arg COMMIT_SHA="$GIT_SHA" \ - --cache-from=type=gha \ - --cache-to=type=gha,mode=max \ - $VITE_STAGING_ARG \ - $(printf -- '--tag %s ' "${TAGS[@]}") \ - . - -echo "Image pushed successfully." diff --git a/.github/scripts/build-image/index.ts b/.github/scripts/build-image/index.ts new file mode 100644 index 000000000..e92ee8ae4 --- /dev/null +++ b/.github/scripts/build-image/index.ts @@ -0,0 +1,112 @@ +import { $ } from "bun"; +import { getEnvVariables } from "load-secrets/env/load"; +import { backend } from "utils/run-backend-instance"; +import { db } from "utils/run-local-db"; + +process.env.TZ = "America/New_York"; + +const tagPrefix = process.env.TAG_PREFIX || ""; +const shouldDockerUpload = Boolean(process.env.DOCKER_UPLOAD) || false; +const serverProfiles = process.env.SERVER_PROFILES || "prod"; + +async function main() { + try { + const ciEnv = await getEnvVariables(["ci"]); + const { dockerHubPat } = parseCiEnv(ciEnv); + const localDbEnv = await db.start(); + const ciAppEnv = await getEnvVariables(["ci-app"]); + await backend.start(ciAppEnv); + + const $$ = $.env({ + ...process.env, + ...Object.fromEntries(ciAppEnv), + ...localDbEnv, + }); + + await $`corepack enable pnpm`; + await $`pnpm --dir js i -D --frozen-lockfile`; + await $$`pnpm --dir js run generate`; + + // copy old tz format from build-image.sh + const timestamp = new Date() + .toLocaleString("en-US", { + timeZone: process.env.TZ, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + .replace(/(\d+)\/(\d+)\/(\d+),\s(\d+):(\d+):(\d+)/, "$3.$1.$2-$4.$5.$6"); + + const gitSha = (await $`git rev-parse --short HEAD`.text()).trim(); + + const tags = [ + `tahminator/codebloom:${tagPrefix}latest`, + `tahminator/codebloom:${tagPrefix}${timestamp}`, + `tahminator/codebloom:${tagPrefix}${gitSha}`, + ]; + + console.log("Building image with following tags:"); + tags.forEach((tag) => console.log(tag)); + + if (dockerHubPat) { + console.log("DOCKER_HUB_PAT found"); + } else { + console.log("DOCKER_HUB_PAT missing or empty"); + } + + await $`echo ${dockerHubPat} | docker login -u tahminator --password-stdin`; + + try { + await $`docker buildx create --use --name codebloom-builder`; + } catch { + await $`docker buildx use codebloom-builder`; + } + + const buildMode = shouldDockerUpload ? "--push" : "--load"; + + const viteStagingArg = + serverProfiles === "stg" ? ["--build-arg", "VITE_STAGING=true"] : []; + + const tagArgs = tags.flatMap((tag) => ["--tag", tag]); + + await $`docker buildx build ${buildMode} \ + --file infra/Dockerfile \ + --build-arg SERVER_PROFILES=${serverProfiles} \ + --build-arg COMMIT_SHA=${gitSha} \ + --cache-from=type=gha \ + --cache-to=type=gha,mode=max \ + ${viteStagingArg} \ + ${tagArgs} \ + .`; + + console.log("Image pushed successfully."); + } finally { + await backend.end(); + await db.end(); + } +} + +function parseCiEnv(ciEnv: Map) { + const dockerHubPat = (() => { + const v = ciEnv.get("DOCKER_HUB_PAT"); + if (!v) { + throw new Error("Missing DOCKER_HUB_PAT from .env.ci"); + } + return v; + })(); + + return { dockerHubPat }; +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/bun.lock b/.github/scripts/bun.lock new file mode 100644 index 000000000..710b29be1 --- /dev/null +++ b/.github/scripts/bun.lock @@ -0,0 +1,469 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "scripts", + "dependencies": { + "@digitalocean/dots": "^1.7.0", + "@notionhq/client": "^5.7.0", + "@octokit/rest": "^22.0.1", + "bun": "^1.3.6", + "octokit": "^5.0.5", + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/bun": "^1.3.6", + "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-perfectionist": "^5.3.1", + "global": "^4.4.0", + "typescript-eslint": "^8.53.0", + }, + }, + }, + "packages": { + "@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], + + "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], + + "@azure/core-http": ["@azure/core-http@3.0.5", "", { "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-tracing": "1.0.0-preview.13", "@azure/core-util": "^1.1.1", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "process": "^0.11.10", "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", "xml2js": "^0.5.0" } }, "sha512-T8r2q/c3DxNu6mEJfPuJtptUVqwchxzjj32gKcnMi06rdiVONS9rar7kT9T2Am+XvER7uOzpsP79WsqNbdgdWg=="], + + "@azure/core-tracing": ["@azure/core-tracing@1.0.0-preview.13", "", { "dependencies": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" } }, "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ=="], + + "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], + + "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + + "@digitalocean/dots": ["@digitalocean/dots@1.7.0", "", { "dependencies": { "@azure/core-http": "^3.0.5", "@microsoft/kiota-authentication-azure": "^1.0.0-preview.61", "@microsoft/kiota-bundle": "^1.0.0-preview.9", "@microsoft/kiota-http-fetchlibrary": "^1.0.0-preview.97", "dotenv": "^17.2.0", "uuid": "^11.1.0" } }, "sha512-4BD52gSlG7xdxMV7sWXTKi9zpl7dmyK+47WNVkLCTw0p5ktlX7VYhV8lDAuCq10fGF4w7LhCh/vOeuB13+x7aQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@microsoft/kiota-abstractions": ["@microsoft/kiota-abstractions@1.0.0-preview.99", "", { "dependencies": { "@opentelemetry/api": "^1.7.0", "@std-uritemplate/std-uritemplate": "^2.0.0", "tinyduration": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-6qrlwGCO3DbvpA7tszqk7JNQPFPDg8gv5NGMTziwdtHqaQrUALKusm3vdJGPVGrbNiZL6/lBaY5NSUvLtlhRCg=="], + + "@microsoft/kiota-authentication-azure": ["@microsoft/kiota-authentication-azure@1.0.0-preview.99", "", { "dependencies": { "@azure/core-auth": "^1.5.0", "@microsoft/kiota-abstractions": "^1.0.0-preview.99", "@opentelemetry/api": "^1.7.0", "tslib": "^2.6.2" } }, "sha512-gPmk/vx15sBMfjfLPgqPanmyVR/rA5HGd3318VtPS28mXxH07Zn30R6PVqSemhIwSxfiGnPiq0SlTj28BQhTKQ=="], + + "@microsoft/kiota-bundle": ["@microsoft/kiota-bundle@1.0.0-preview.99", "", { "dependencies": { "@microsoft/kiota-abstractions": "^1.0.0-preview.99", "@microsoft/kiota-http-fetchlibrary": "^1.0.0-preview.99", "@microsoft/kiota-serialization-form": "^1.0.0-preview.99", "@microsoft/kiota-serialization-json": "^1.0.0-preview.99", "@microsoft/kiota-serialization-multipart": "^1.0.0-preview.99", "@microsoft/kiota-serialization-text": "^1.0.0-preview.99" } }, "sha512-AxvO+z6UgWMAT2NfXN36CMhAUm/39tUQt8o32axeEJDS/EvZINDAPstbQV+zK7bJRC8kCqUHhCE/fuHX2dXA+g=="], + + "@microsoft/kiota-http-fetchlibrary": ["@microsoft/kiota-http-fetchlibrary@1.0.0-preview.99", "", { "dependencies": { "@microsoft/kiota-abstractions": "^1.0.0-preview.99", "@opentelemetry/api": "^1.7.0", "tslib": "^2.6.2" } }, "sha512-lIruiYf8L7DPG0Fm92QN5YK4zx4sh76EtTONUAqBCxt5AtEJ7KQceBajaXQsjfcndUAp9LmDVdqR44o8sc9/mA=="], + + "@microsoft/kiota-serialization-form": ["@microsoft/kiota-serialization-form@1.0.0-preview.99", "", { "dependencies": { "@microsoft/kiota-abstractions": "^1.0.0-preview.99", "tslib": "^2.6.2" } }, "sha512-BdXxqNfy+5yWTyxpBguqJYy6E9RRotrDClRcUO89okFcR5N6s6xenjsvGw++q9KWNi7qcHrqaG8xlRz1TLk81Q=="], + + "@microsoft/kiota-serialization-json": ["@microsoft/kiota-serialization-json@1.0.0-preview.99", "", { "dependencies": { "@microsoft/kiota-abstractions": "^1.0.0-preview.99", "tslib": "^2.6.2" } }, "sha512-kDmMYmB7XkRprlLviEynRPDkE45JDxqiuUopfBRMvNK4qhzFiJTXGLihZ2FG6fdVu9LbV3/W0QxbeGP35kDK6A=="], + + "@microsoft/kiota-serialization-multipart": ["@microsoft/kiota-serialization-multipart@1.0.0-preview.99", "", { "dependencies": { "@microsoft/kiota-abstractions": "^1.0.0-preview.99", "tslib": "^2.6.2" } }, "sha512-cfviCAVTlZPD+MUgdTIgsX/IfHND+gKQhem3vJeo7l5Qqz2rvvVFXOHwECI19umO9Un1QFKe1V5wg+M+aj90+g=="], + + "@microsoft/kiota-serialization-text": ["@microsoft/kiota-serialization-text@1.0.0-preview.99", "", { "dependencies": { "@microsoft/kiota-abstractions": "^1.0.0-preview.99", "tslib": "^2.6.2" } }, "sha512-gcBffQRI1soHVAiS4YjfMr3SQ9fC8xVUGMfarTTszU4PjpxyFOknaWoFdt/0rvMeavI9jOtbyvDKAf77cZyYIQ=="], + + "@notionhq/client": ["@notionhq/client@5.7.0", "", {}, "sha512-On/GI10lJRLSrzU3h/cDl1Oe15qywYrEvyV3KlE4UgtzqMjlPzXG31B7zHjGc/tTgoY8eD3JS4CWlcD64NBmaQ=="], + + "@octokit/app": ["@octokit/app@16.1.2", "", { "dependencies": { "@octokit/auth-app": "^8.1.2", "@octokit/auth-unauthenticated": "^7.0.3", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ=="], + + "@octokit/auth-app": ["@octokit/auth-app@8.1.2", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-db8VO0PqXxfzI6GdjtgEFHY9tzqUql5xMFXYA12juq8TeTgPAuiiP3zid4h50lwlIP457p5+56PnJOgd2GGBuw=="], + + "@octokit/auth-oauth-app": ["@octokit/auth-oauth-app@9.0.3", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg=="], + + "@octokit/auth-oauth-device": ["@octokit/auth-oauth-device@8.0.3", "", { "dependencies": { "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw=="], + + "@octokit/auth-oauth-user": ["@octokit/auth-oauth-user@6.0.2", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A=="], + + "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/auth-unauthenticated": ["@octokit/auth-unauthenticated@7.0.3", "", { "dependencies": { "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g=="], + + "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + + "@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/oauth-app": ["@octokit/oauth-app@8.0.3", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.2", "@octokit/auth-oauth-user": "^6.0.1", "@octokit/auth-unauthenticated": "^7.0.2", "@octokit/core": "^7.0.5", "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/oauth-methods": "^6.0.1", "@types/aws-lambda": "^8.10.83", "universal-user-agent": "^7.0.0" } }, "sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg=="], + + "@octokit/oauth-authorization-url": ["@octokit/oauth-authorization-url@8.0.0", "", {}, "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ=="], + + "@octokit/oauth-methods": ["@octokit/oauth-methods@6.0.2", "", { "dependencies": { "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/openapi-webhooks-types": ["@octokit/openapi-webhooks-types@12.1.0", "", {}, "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA=="], + + "@octokit/plugin-paginate-graphql": ["@octokit/plugin-paginate-graphql@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="], + + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="], + + "@octokit/plugin-retry": ["@octokit/plugin-retry@8.0.3", "", { "dependencies": { "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": ">=7" } }, "sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA=="], + + "@octokit/plugin-throttling": ["@octokit/plugin-throttling@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^7.0.0" } }, "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg=="], + + "@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="], + + "@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/webhooks": ["@octokit/webhooks@14.2.0", "", { "dependencies": { "@octokit/openapi-webhooks-types": "12.1.0", "@octokit/request-error": "^7.0.0", "@octokit/webhooks-methods": "^6.0.0" } }, "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw=="], + + "@octokit/webhooks-methods": ["@octokit/webhooks-methods@6.0.0", "", {}, "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27rypIapNkYboOSylkf1tD9UW9Ado2I+P1NBL46Qz29KmOjTL6WuJ7mHDC5O66CYxlOkF5r93NPDAC3lFHYBXw=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-I82xGzPkBxzBKgbl8DsA0RfMQCWTWjNmLjIEkW1ECiv3qK02kHGQ5FGUr/29L/SuvnGsULW4tBTRNZiMzL37nA=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-nqtr+pTsHqusYpG2OZc6s+AmpWDB/FmBvstrK0y5zkti4OqnCuu7Ev2xNjS7uyb47NrAFF40pWqkpaio5XEd7w=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-YaQEAYjBanoOOtpqk/c5GGcfZIyxIIkQ2m1TbHjedRmJNwxzWBhGinSARFkrRIc3F8pRIGAopXKvJ/2rjN1LzQ=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-FR+iJt17rfFgYgpxL3M67AUwujOgjw52ZJzB9vElI5jQXNjTyOKf8eH4meSk4vjlYF3h/AjKYd6pmN0OIUlVKQ=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-egfngj0dfJ868cf30E7B+ye9KUWSebYxOG4l9YP5eWeMXCtenpenx0zdKtAn9qxJgEJym5AN6trtlk+J6x8Lig=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-jRmnX18ak8WzqLrex3siw0PoVKyIeI5AiCv4wJLgSs7VKfOqrPycfHIWfIX2jdn7ngqbHFPzI09VBKANZ4Pckg=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-YeXcJ9K6vJAt1zSkeA21J6pTe7PgDMLTHKGI3nQBiMYnYf7Ob3K+b/ChSCznrJG7No5PCPiQPg4zTgA+BOTmSA=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-7FjVnxnRTp/AgWqSQRT/Vt9TYmvnZ+4M+d9QOKh/Lf++wIFXFGSeAgD6bV1X/yr2UPVmZDk+xdhr2XkU7l2v3w=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-Sr1KwUcbB0SEpnSPO22tNJppku2khjFluEst+mTGhxHzAGQTQncNeJxDnt3F15n+p9Q+mlcorxehd68n1siikQ=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-PFUa7JL4lGoyyppeS4zqfuoXXih+gSE0XxhDMrCPVEUev0yhGNd/tbWBvcdpYnUth80owENoGjc8s5Knopv9wA=="], + + "@std-uritemplate/std-uritemplate": ["@std-uritemplate/std-uritemplate@2.0.8", "", {}, "sha512-8oj7jKksoTRxjdPkWKX9FyOKDS8ORBm+ZfTSHm0f3eYha8cI0O1d57gyduOIAX7Y2uUukNDNlxlvGb+FFkE7yQ=="], + + "@types/aws-lambda": ["@types/aws-lambda@8.10.159", "", {}, "sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg=="], + + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + + "@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw=="], + + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + + "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "bun": ["bun@1.3.6", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.6", "@oven/bun-darwin-x64": "1.3.6", "@oven/bun-darwin-x64-baseline": "1.3.6", "@oven/bun-linux-aarch64": "1.3.6", "@oven/bun-linux-aarch64-musl": "1.3.6", "@oven/bun-linux-x64": "1.3.6", "@oven/bun-linux-x64-baseline": "1.3.6", "@oven/bun-linux-x64-musl": "1.3.6", "@oven/bun-linux-x64-musl-baseline": "1.3.6", "@oven/bun-windows-x64": "1.3.6", "@oven/bun-windows-x64-baseline": "1.3.6" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-Tn98GlZVN2WM7+lg/uGn5DzUao37Yc0PUz7yzYHdeF5hd+SmHQGbCUIKE4Sspdgtxn49LunK3mDNBC2Qn6GJjw=="], + + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dom-walk": ["dom-walk@0.1.2", "", {}, "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="], + + "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "eslint-plugin-no-relative-import-paths": ["eslint-plugin-no-relative-import-paths@1.6.1", "", {}, "sha512-YZNeOnsOrJcwhFw0X29MXjIzu2P/f5X2BZDPWw1R3VUYBRFxNIh77lyoL/XrMU9ewZNQPcEvAgL/cBOT1P330A=="], + + "eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@5.3.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.52.0", "natural-orderby": "^5.0.0" }, "peerDependencies": { "eslint": ">=8.45.0" } }, "sha512-v8kAP8TarQYqDC4kxr343ZNi++/oOlBnmWovsUZpbJ7A/pq1VHGlgsf/fDh4CdEvEstzkrc8NLvoVKtfpsC4oA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "global": ["global@4.4.0", "", { "dependencies": { "min-document": "^2.19.0", "process": "^0.11.10" } }, "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "min-document": ["min-document@2.19.2", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "natural-orderby": ["natural-orderby@5.0.0", "", {}, "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "octokit": ["octokit@5.0.5", "", { "dependencies": { "@octokit/app": "^16.1.2", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-graphql": "^6.0.0", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0", "@octokit/plugin-retry": "^8.0.3", "@octokit/plugin-throttling": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinyduration": ["tinyduration@3.4.1", "", {}, "sha512-NemFoamVYn7TmtwZKZ3OiliM9fZkr6EWiTM+wKknco6POSy2gS689xx/pXip0JYp40HXpUw6k65CUYHWYUXdaA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "universal-github-app-jwt": ["universal-github-app-jwt@2.2.2", "", {}, "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw=="], + + "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@azure/core-auth/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + + "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "@azure/core-util/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + } +} diff --git a/.github/scripts/eslint.config.js b/.github/scripts/eslint.config.js new file mode 100644 index 000000000..05a3bde39 --- /dev/null +++ b/.github/scripts/eslint.config.js @@ -0,0 +1,52 @@ +import js from "@eslint/js"; +import noRelativeImportPaths from "eslint-plugin-no-relative-import-paths"; +import perfectionist from "eslint-plugin-perfectionist"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + plugins: { + perfectionist, + }, + rules: { + "perfectionist/sort-imports": "error", + }, + }, + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "no-relative-import-paths": noRelativeImportPaths, + }, + settings: { + "import/resolver": { + alias: { + map: [["@", "./src"]], + extensions: [".ts", ".tsx", ".js", ".jsx"], + }, + }, + }, + rules: { + "no-relative-import-paths/no-relative-import-paths": [ + "error", + { allowSameFolder: false, rootDir: "src", prefix: "@" }, + ], + "@typescript-eslint/no-namespace": ["off"], + "@typescript-eslint/no-non-null-assertion": ["error"], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, +); diff --git a/.github/scripts/load-secrets.sh b/.github/scripts/load-secrets.sh deleted file mode 100644 index 860495abf..000000000 --- a/.github/scripts/load-secrets.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -git-crypt unlock - -# UNLOAD_ENVIRONMENTS="prod,staging,dev" -IFS=',' read -ra ENVS <<<"${UNLOAD_ENVIRONMENTS:-}" - -declare -A LOADED - -for v in "${ENVS[@]}"; do - ENV_FILE=".env.${v}" - if [[ -f "$ENV_FILE" ]]; then - echo "Loading $ENV_FILE" - declare -A BEFORE - for VAR in $(compgen -v); do - BEFORE["$VAR"]=1 - done - - source "$ENV_FILE" - - for VAR in $(compgen -v); do - if [[ -z "${BEFORE["$VAR"]:-}" ]]; then - LOADED["$VAR"]=1 - fi - done - else - echo "Warning: $ENV_FILE not found" - fi -done - -EXCLUDED_VARS=( - "PATH" - "HOME" - "PWD" - "SHELL" - "USER" - "DEBUG" - "LOG_LEVEL" - "CI" - "JAVA_HOME" -) - -for VAR in "${!LOADED[@]}"; do - VALUE="${!VAR-}" - - if [[ "$VAR" == "VAR" ]]; then # weird bug - continue - fi - - echo "$VAR=$VALUE" >>"$GITHUB_ENV" - - for EX in "${EXCLUDED_VARS[@]}"; do - if [[ "$VAR" == "$EX" ]]; then - echo "Not masking $VAR: Excluded" - continue 2 - fi - done - - if [[ "$VALUE" == "true" || "$VALUE" == "false" || -z "$VALUE" ]]; then - echo "Not masking $VAR: true/false/empty value" - continue - fi - - echo "Masking $VAR" - echo "::add-mask::$VALUE" -done diff --git a/.github/scripts/load-secrets/env/load.ts b/.github/scripts/load-secrets/env/load.ts new file mode 100644 index 000000000..b2cb5eae7 --- /dev/null +++ b/.github/scripts/load-secrets/env/load.ts @@ -0,0 +1,72 @@ +import { $ } from "bun"; + +let isGitCryptUnlocked = false; + +/** + * @param environments - List of environment files to load. + * @param mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING - Should variables be masked. Defaults to `true`. __NOTE: This will only work in a GitHub Action runner.__ + * + * @returns a map of the loaded environments as a key and value inside of a map. + * + * _Please note that duplicate environment variables will be overwritten, so + * the order in which you define `environments` does matter._ + */ +export async function getEnvVariables( + environments: string[], + mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING = true, +): Promise> { + if (!isGitCryptUnlocked) { + await $`git-crypt unlock`; + isGitCryptUnlocked = true; + } + + const loaded = new Map(); + + for (const env of environments) { + const envFile = Bun.file(`.env.${env}`); + if (await envFile.exists()) { + console.log(`Loading ${envFile.name}`); + + const content = await envFile.text(); + const lines = content.split("\n").filter((s) => s.length > 0); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + const match = trimmed.split("=").filter((s) => s.length > 0); + if (match.length === 2) { + const [key, value] = match as [string, string]; + const cleanKey = key.trim(); + let cleanValue = value.trim(); + if ( + (cleanValue.startsWith('"') && cleanValue.endsWith('"')) || + (cleanValue.startsWith("'") && cleanValue.endsWith("'")) + ) { + cleanValue = cleanValue.slice(1, -1); + } + + if (!loaded.has(cleanKey)) { + loaded.set(cleanKey, cleanValue); + } + } + } + } else { + console.warn(`Warning: ${envFile.name} not found`); + } + } + + if (mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING) { + for (const [varName, value] of loaded.entries()) { + if (value === "true" || value === "false" || value === "") { + console.log(`Not masking ${varName}: true/false/empty value`); + continue; + } + + console.log(`Masking ${varName}`); + console.log(`::add-mask::${value}`); + } + } + + return loaded; +} diff --git a/.github/scripts/load-secrets/index.ts b/.github/scripts/load-secrets/index.ts new file mode 100644 index 000000000..1800678bd --- /dev/null +++ b/.github/scripts/load-secrets/index.ts @@ -0,0 +1,63 @@ +import { getEnvVariables } from "load-secrets/env/load"; + +// UNLOAD_ENVIRONMENTS="prod,staging,dev" +const unloadEnvironments = process.env.UNLOAD_ENVIRONMENTS || ""; + +const excludedVars = [ + "PATH", + "HOME", + "PWD", + "SHELL", + "USER", + "DEBUG", + "LOG_LEVEL", + "CI", + "JAVA_HOME", +]; + +/** + * @deprecated this is no longer a supported flow. + */ +async function main() { + const envs = unloadEnvironments + .split(",") + .map((e) => e.trim()) + .filter(Boolean); + + const loaded = await getEnvVariables(envs, false); + + const githubEnv = process.env.GITHUB_ENV; + if (!githubEnv) { + console.log("Warning: GITHUB_ENV not set, skipping variable export"); + return; + } + + const githubEnvFileWriter = Bun.file(githubEnv).writer(); + + for (const [varName, value] of loaded.entries()) { + githubEnvFileWriter.write(`${varName}=${value}\n`); + if (excludedVars.includes(varName)) { + console.log(`Not masking ${varName}: Excluded`); + continue; + } + + if (value === "true" || value === "false" || value === "") { + console.log(`Not masking ${varName}: true/false/empty value`); + continue; + } + + console.log(`Masking ${varName}`); + console.log(`::add-mask::${value}`); + } + + githubEnvFileWriter.flush(); +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/local-db.sh b/.github/scripts/local-db.sh deleted file mode 100644 index 2decab01d..000000000 --- a/.github/scripts/local-db.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -db_cleanup() { - echo "Stopping and removing postgres container..." - docker stop codebloom-db >/dev/null 2>&1 || true - docker rm codebloom-db >/dev/null 2>&1 || true - - unset DATABASE_HOST - unset DATABASE_PORT - unset DATABASE_NAME - unset DATABASE_USER - unset DATABASE_PASSWORD -} - -db_startup() { - echo "Starting postgres container..." - docker rm -f codebloom-db >/dev/null 2>&1 || true - docker run -d \ - --name codebloom-db \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ - -e POSTGRES_DB=codebloom \ - -p 5440:5432 \ - postgres:16 - - echo "Waiting for postgres to become ready." - for i in {1..30}; do - if docker exec codebloom-db pg_isready -U postgres >/dev/null 2>&1; then - echo "postgres is ready!" - break - fi - echo "Waiting for postgres, sleep 2... ($i/30)" - sleep 2 - done - - if ! docker exec codebloom-db pg_isready -U postgres >/dev/null 2>&1; then - echo "postgres failed to start in time." - docker logs codebloom-db || true - exit 1 - fi - - export DATABASE_HOST=localhost - export DATABASE_PORT=5440 - export DATABASE_NAME=codebloom - export DATABASE_USER=postgres - export DATABASE_PASSWORD=postgres - - echo "postgres ready. migrating now..." - ./mvnw flyway:migrate -Dflyway.locations=filesystem:./db - echo "postgres migration complete" - -} diff --git a/.github/scripts/notion/commits/index.ts b/.github/scripts/notion/commits/index.ts new file mode 100644 index 000000000..12bc8ec50 --- /dev/null +++ b/.github/scripts/notion/commits/index.ts @@ -0,0 +1,49 @@ +import { $ } from "bun"; +import { sendMessage } from "utils/send-message"; + +export async function _checkCommits(taskId: number, prId: number) { + const taskIdString = taskId.toString(); + + const res = await $`gh pr view ${prId} --json commits`.text(); + const { commits } = JSON.parse(res) as { + commits?: { messageHeadline: string }[]; + }; + + if (!commits || commits.length === 0) { + console.log("No commits found in PR"); + return; + } + + const failedCommits = commits + .map((commit) => commit.messageHeadline) + .map((s) => s.trim()) + .filter((message) => !message.startsWith(taskIdString)); + + if (failedCommits.length == 0) { + console.log("All commits are valid"); + return; + } + + for (const message of failedCommits) { + console.warn(`Invalid commit: ${message}`); + } + + const failedList = failedCommits.join("\n"); + + await sendMessage( + prId, + ` +### Commit Validation Failed +The following commits do not start with the required Notion ID \`${taskIdString}\`: + +\`\`\` +${failedList} +\`\`\` + +Please rebase and update your commit messages. +All messages should be of the following format: \`${taskIdString}: Example commit\``.trim(), + ); + + console.error("One or more commits do not match the Notion ID."); + process.exit(1); +} diff --git a/.github/scripts/notion/index.ts b/.github/scripts/notion/index.ts new file mode 100644 index 000000000..ea7ada566 --- /dev/null +++ b/.github/scripts/notion/index.ts @@ -0,0 +1,75 @@ +import { getEnvVariables } from "load-secrets/env/load"; +import { _checkCommits } from "notion/commits"; +import { checkNotionPrAndGetTask } from "notion/pr"; + +export * from "./pr"; + +const prId = (() => { + const v = process.env.PR_ID; + if (!v) { + throw new Error("PR_ID is required"); + } + const n = Number(v); + if (isNaN(n)) { + throw new Error("PR_ID is not a number"); + } + return n; +})(); + +const getGhaOutput = process.env.GET_GHA_OUTPUT === "true"; +const githubOutputFile = process.env.GITHUB_OUTPUT; + +async function main() { + const { notionDbId, notionSecret } = parseCiEnv( + await getEnvVariables(["ci"]), + ); + + const { taskId, taskContent } = await checkNotionPrAndGetTask( + notionSecret, + prId, + notionDbId, + ); + + console.log(taskContent); + + if (getGhaOutput && githubOutputFile) { + const w = Bun.file(githubOutputFile).writer(); + await w.write(`context<) { + const notionDbId = (() => { + const v = ciEnv.get("NOTION_TASK_DB_ID"); + if (!v) { + throw new Error("Missing NOTION_TASK_DB_ID from .env.ci"); + } + return v; + })(); + + const notionSecret = (() => { + const v = ciEnv.get("NOTION_SECRET"); + if (!v) { + throw new Error("Missing NOTION_SECRET from .env.ci"); + } + return v; + })(); + + return { + notionDbId, + notionSecret, + }; +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/notion/pr/index.ts b/.github/scripts/notion/pr/index.ts new file mode 100644 index 000000000..a95ff889e --- /dev/null +++ b/.github/scripts/notion/pr/index.ts @@ -0,0 +1,52 @@ +import type { NotionTaskObject } from "notion/pr/types"; + +import { Client, isFullPage } from "@notionhq/client"; +import { $ } from "bun"; +import { _fetchBlocks } from "notion/pr/utils"; +import { sendMessage } from "utils/send-message"; + +import { _getNotionTaskById } from "../task"; + +export async function checkNotionPrAndGetTask( + notionPat: string, + prId: number, + notionDbId: string, +): Promise { + const client = new Client({ + auth: notionPat, + }); + + const title = await (async () => { + const res = await $`gh pr view ${prId} --json title`.text(); + const json = JSON.parse(res) as { title?: string } | undefined; + + if (!json || !json.title) { + await sendMessage(prId, "Failed to parse Notion ID"); + process.exit(1); + } + + return json.title; + })(); + + const ticketNum = parseInt(title, 10); + + if (!Number.isNaN(ticketNum) && (ticketNum <= 0 || ticketNum > 999)) { + await sendMessage(prId, `No numeric prefix found in PR title: ${title}`); + process.exit(1); + } + + const task = await _getNotionTaskById(client, notionDbId, ticketNum); + + if (!isFullPage(task)) { + sendMessage(prId, "Notion task is not expected page type"); + process.exit(1); + } + + const blocks = await _fetchBlocks(client, task.id); + + return { + task, + taskContent: blocks.join("\n"), + taskId: ticketNum, + }; +} diff --git a/.github/scripts/notion/pr/types.ts b/.github/scripts/notion/pr/types.ts new file mode 100644 index 000000000..14f950cff --- /dev/null +++ b/.github/scripts/notion/pr/types.ts @@ -0,0 +1,7 @@ +import { type PageObjectResponse } from "@notionhq/client"; + +export type NotionTaskObject = { + task: PageObjectResponse; + taskContent: string; + taskId: number; +}; diff --git a/.github/scripts/notion/pr/utils.ts b/.github/scripts/notion/pr/utils.ts new file mode 100644 index 000000000..9c51134b7 --- /dev/null +++ b/.github/scripts/notion/pr/utils.ts @@ -0,0 +1,90 @@ +import type { + BlockObjectResponse, + RichTextItemResponse, +} from "@notionhq/client/build/src/api-endpoints"; + +import { isFullBlock, type Client } from "@notionhq/client"; + +export async function _fetchBlocks( + client: Client, + blockId: string, + indent = "", +): Promise { + const response = await client.blocks.children.list({ + block_id: blockId, + }); + + console.log(response); + + const lines: string[] = []; + + for (const block of response.results) { + if (!isFullBlock(block)) continue; + + const markdown = blockToMarkdown(block, indent); + + if (markdown) { + lines.push(markdown); + } + + if (block.has_children) { + const childLines = await _fetchBlocks(client, block.id, indent + " "); + lines.push(...childLines); + } + } + + return lines; +} + +function extractPlainText(richText: RichTextItemResponse[]): string { + return richText.map((item) => item.plain_text).join(""); +} + +function blockToMarkdown(block: BlockObjectResponse, indent: string): string { + switch (block.type) { + case "paragraph": + return indent + extractPlainText(block.paragraph.rich_text); + + case "bulleted_list_item": + return ( + indent + "- " + extractPlainText(block.bulleted_list_item.rich_text) + ); + + case "numbered_list_item": + return ( + indent + "1. " + extractPlainText(block.numbered_list_item.rich_text) + ); + + case "heading_1": + return indent + "# " + extractPlainText(block.heading_1.rich_text); + + case "heading_2": + return indent + "## " + extractPlainText(block.heading_2.rich_text); + + case "heading_3": + return indent + "### " + extractPlainText(block.heading_3.rich_text); + + case "quote": + return indent + "> " + extractPlainText(block.quote.rich_text); + + case "to_do": { + const checked = block.to_do.checked ? "[x]" : "[ ]"; + return indent + checked + " " + extractPlainText(block.to_do.rich_text); + } + + case "toggle": + return indent + extractPlainText(block.toggle.rich_text); + + case "callout": + return indent + extractPlainText(block.callout.rich_text); + + case "code": { + const language = block.code.language || ""; + const code = extractPlainText(block.code.rich_text); + return `${indent}\`\`\`${language}\n${code}\n${indent}\`\`\``; + } + + default: + return ""; + } +} diff --git a/.github/scripts/notion/task/index.ts b/.github/scripts/notion/task/index.ts new file mode 100644 index 000000000..8443dfa91 --- /dev/null +++ b/.github/scripts/notion/task/index.ts @@ -0,0 +1,30 @@ +import { Client } from "@notionhq/client"; + +export async function _getNotionTaskById( + client: Client, + dbId: string, + id: number, +) { + try { + const { results } = await client.dataSources.query({ + filter: { + property: "ID", + unique_id: { + equals: id, + }, + }, + data_source_id: dbId, + }); + + const ticket = results[0]; + + if (!ticket) { + throw new Error("Ticket with ID does not exist."); + } + + return ticket; + } catch (e) { + console.error("Ticket cannot be retrieved\n", e); + process.exit(-100); + } +} diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 000000000..7095bfb90 --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,24 @@ +{ + "name": "scripts", + "version": "1.0.0", + "description": "CodeBloom CI Scripts", + "scripts": {}, + "keywords": [], + "author": "Tahmid Ahmed", + "license": "MIT", + "dependencies": { + "@digitalocean/dots": "^1.7.0", + "@notionhq/client": "^5.7.0", + "@octokit/rest": "^22.0.1", + "bun": "^1.3.6", + "octokit": "^5.0.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/bun": "^1.3.6", + "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-perfectionist": "^5.3.1", + "global": "^4.4.0", + "typescript-eslint": "^8.53.0" + } +} diff --git a/.github/scripts/redeploy/db/index.ts b/.github/scripts/redeploy/db/index.ts new file mode 100644 index 000000000..44384bfa8 --- /dev/null +++ b/.github/scripts/redeploy/db/index.ts @@ -0,0 +1,30 @@ +import type { Environment } from "types"; + +import { $ } from "bun"; + +export async function _migrateDb({ + environment, + env, + sha, +}: { + environment: Environment; + env: Map; + sha?: string; +}): Promise { + await $`git fetch origin main:main`; + + if (environment === "staging") { + const diffOutput = await $`git diff --name-only main...${sha}`.text(); + const files = diffOutput.split("\n"); + + const hasDbChanges = files.some((file) => file.startsWith("db/")); + if (!hasDbChanges) { + console.log("in staging, skipping db migration."); + return; + } + } + + await $.env( + Object.fromEntries(env), + )`./mvnw flyway:migrate -Dflyway.locations=filesystem:./db/migration`; +} diff --git a/.github/scripts/redeploy/do/apps/create.ts b/.github/scripts/redeploy/do/apps/create.ts new file mode 100644 index 000000000..fc9d65b28 --- /dev/null +++ b/.github/scripts/redeploy/do/apps/create.ts @@ -0,0 +1,25 @@ +import type { App_spec } from "@digitalocean/dots"; +import type { DigitalOceanClient } from "@digitalocean/dots/src/dots/digitalOceanClient"; + +export async function _createAppAndgetAppId( + client: DigitalOceanClient, + projectId: string, + spec: App_spec, +): Promise { + const res = await client.v2.apps.post({ + spec, + projectId, + }); + + if (!res) { + return null; + } + + const { app } = res; + + if (!app) { + return null; + } + + return app.id ?? null; +} diff --git a/.github/scripts/redeploy/do/apps/get.ts b/.github/scripts/redeploy/do/apps/get.ts new file mode 100644 index 000000000..be43a1c05 --- /dev/null +++ b/.github/scripts/redeploy/do/apps/get.ts @@ -0,0 +1,40 @@ +import type { App_spec } from "@digitalocean/dots"; +import type { DigitalOceanClient } from "@digitalocean/dots/src/dots/digitalOceanClient"; + +export async function _getAppId( + client: DigitalOceanClient, + projectId: string, + spec: App_spec, +): Promise { + const appName = spec.name; + + if (!appName) { + throw new Error("App spec name missing, can't find app"); + } + + const res = await client.v2.apps.get({ + queryParameters: { + withProjects: true, + }, + }); + + if (!res) { + return null; + } + + const { apps } = res; + + if (!apps) { + return null; + } + + const foundApp = apps.filter( + (app) => app.projectId == projectId && app.spec?.name == appName, + )[0]; + + if (!foundApp) { + return null; + } + + return foundApp.id ?? null; +} diff --git a/.github/scripts/redeploy/do/index.ts b/.github/scripts/redeploy/do/index.ts new file mode 100644 index 000000000..bfe1618a3 --- /dev/null +++ b/.github/scripts/redeploy/do/index.ts @@ -0,0 +1,109 @@ +import type { Environment } from "types"; + +import { + DigitalOceanApiKeyAuthenticationProvider, + FetchRequestAdapter, + createDigitalOceanClient, + type App_variable_definition, + type App_response, +} from "@digitalocean/dots"; +import { _createAppAndgetAppId } from "redeploy/do/apps/create"; +import { _getAppId } from "redeploy/do/apps/get"; + +import { prodSpec, stgSpec } from "../../../../.do/specs"; + +export async function _migrateDo({ + token, + environment, + projectId, + env, +}: { + token: string; + environment: Environment; + projectId: string; + env: Map; +}): Promise { + const authProvider = new DigitalOceanApiKeyAuthenticationProvider(token); + const adapter = new FetchRequestAdapter(authProvider); + const client = createDigitalOceanClient(adapter); + + const envs: App_variable_definition[] = env + .entries() + .map(([key, value]) => { + const env: App_variable_definition = { + key, + value, + scope: "RUN_TIME", + type: "SECRET", + }; + return env; + }) + .toArray(); + + const spec = environment === "staging" ? stgSpec(envs) : prodSpec(envs); + + const appId = await (async () => { + const v = await _getAppId(client, projectId, spec); + + if (v) { + return v; + } + + const v2 = await _createAppAndgetAppId(client, projectId, spec); + if (!v2) { + throw new Error( + "App not found and failed to create as well. Please alert Codebloom team.", + ); + } + + return v2; + })(); + + let res: App_response | undefined; + try { + res = await client.v2.apps.byApp_Id(appId).put({ + spec, + updateAllSourceVersions: true, + }); + } catch (e) { + console.error(e); + return process.exit(1); + } + + const pendingDeploymentId = res?.app?.pendingDeployment?.id; + + if (!pendingDeploymentId) { + console.error("Failed to find pending deployment."); + process.exit(1); + } + + console.log("Deployment ID:", pendingDeploymentId); + + let ready = false; + const attempts = 60; + + for (let i = 1; i <= attempts; i++) { + const res = await client.v2.apps + .byApp_Id(appId) + .deployments.byDeployment_id(pendingDeploymentId) + .get(); + + const phase = res?.deployment?.phase ?? "UNKNOWN"; + + console.log("Deployment phase:", phase); + + if (phase === "ACTIVE") { + console.log("Deployment has completed!"); + ready = true; + break; + } + + console.log(`Waiting for deployment to complete... (${i}/${attempts})`); + await Bun.sleep(10000); + } + + if (!ready) { + console.error("Deployment did not reach a valid state within 10 minutes."); + process.exit(1); + } +} diff --git a/.github/scripts/redeploy/index.ts b/.github/scripts/redeploy/index.ts new file mode 100644 index 000000000..ab35d747b --- /dev/null +++ b/.github/scripts/redeploy/index.ts @@ -0,0 +1,83 @@ +import type { Environment } from "types"; + +import { getEnvVariables } from "load-secrets/env/load"; +import { _migrateDb } from "redeploy/db"; +import { _migrateDo } from "redeploy/do"; + +const environment: Environment = (() => { + const v = process.env.ENVIRONMENT; + if (!v) { + throw new Error("Environment is required"); + } + + if (v !== "staging" && v !== "production") { + throw new Error( + 'Environment must be the string literal "staging" or "production"', + ); + } + + return v; +})(); + +const sha = (() => { + const v = process.env.SHA; + if (environment === "staging" && !v) { + throw new Error( + "SHA must be available in ENV if script is being run in staging environment.", + ); + } + return v; +})(); + +async function main() { + // should already be called + // await $`git-crypt unlock`; + + const { token, projectId } = parseCiEnv(await getEnvVariables(["ci"])); + const appEnv = await getEnvVariables([environment]); + + await _migrateDb({ + env: appEnv, + environment, + sha, + }); + + await _migrateDo({ + projectId, + token, + environment, + env: appEnv, + }); +} + +function parseCiEnv(ciEnv: Map) { + const token = (() => { + const v = ciEnv.get("DIGITALOCEAN_PAT"); + if (!v) { + throw new Error("Missing DIGITALOCEAN_PAT from .env.ci"); + } + return v; + })(); + + const projectId = (() => { + const v = ciEnv.get("DIGITALOCEAN_PROJECT_ID"); + if (!v) { + throw new Error("Missing DIGITALOCEAN_PROJECT_ID from .env.ci"); + } + return v; + })(); + + return { + token, + projectId, + }; +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/run-backend-compile-tests.sh b/.github/scripts/run-backend-compile-tests.sh deleted file mode 100644 index 1f9e29aa7..000000000 --- a/.github/scripts/run-backend-compile-tests.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -RED='\033[0;31m' - -# fmt -./mvnw spotless:check - -# lint -./mvnw checkstyle:check - -# compile -./mvnw -B verify -Dmaven.test.skip=true --no-transfer-progress diff --git a/.github/scripts/run-backend-instance.sh b/.github/scripts/run-backend-instance.sh deleted file mode 100644 index 5c9a5549a..000000000 --- a/.github/scripts/run-backend-instance.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -backend_cleanup() { - echo "[INFO] ===== BACKEND LOGS START =====" - cat backend.log || echo "[WARN] backend.log not found" - echo "[INFO] ===== BACKEND LOGS END =====" - if kill $(cat spring_pid.txt) >/dev/null 2>&1; then - echo "Backend process killed successfully." - else - echo "Backend was not running or already stopped." - fi -} - -backend_startup() { - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - ./mvnw -Dspring-boot.run.profiles=ci spring-boot:run >backend.log 2>&1 & - echo $! >spring_pid.txt - - backend_started=false - for i in {1..30}; do - if curl -s http://localhost:8080/api | grep -q '"success":true'; then - echo "Backend is up!" - backend_started=true - break - fi - echo "Waiting for backend... ($i/30)" - sleep 5 - done - - if [ "$backend_started" = false ]; then - echo "Backend failed to start in time." - exit 1 - fi - -} diff --git a/.github/scripts/run-backend-tests.sh b/.github/scripts/run-backend-tests.sh deleted file mode 100644 index 9c9575178..000000000 --- a/.github/scripts/run-backend-tests.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/local-db.sh" -source "$DIR/run-backend-instance.sh" -source "$DIR/run-frontend-instance.sh" - -trap 'db_cleanup; backend_cleanup; frontend_cleanup' EXIT - -java -version -javac -version -echo "JAVA_HOME=$JAVA_HOME" - -./mvnw -B install -D skipTests --no-transfer-progress - -./mvnw -B exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install-deps" -./mvnw -B exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install firefox" - -corepack enable pnpm -cd email -pnpm i --frozen-lockfile -./email.sh -cd .. - -db_startup - -backend_startup - -frontend_startup - -backend_cleanup - -./mvnw clean verify -Dspring.profiles.active=ci \ No newline at end of file diff --git a/.github/scripts/run-frontend-instance.sh b/.github/scripts/run-frontend-instance.sh deleted file mode 100644 index b681c5d6d..000000000 --- a/.github/scripts/run-frontend-instance.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash - -frontend_cleanup() { - echo "[INFO] ===== FRONTEND LOGS START =====" - cat frontend.log || echo "[WARN] frontend.log not found" - echo "[INFO] ===== FRONTEND LOGS END =====" - if kill $(cat frontend_pid.txt) >/dev/null 2>&1; then - echo "Frontend process killed successfully." - else - echo "Frontend was not running or already stopped." - fi -} - -frontend_startup() { - corepack enable pnpm - cd js - pnpm i --frozen-lockfile - - echo "Generating API schema..." - pnpm run generate - - if [ ! -f "src/lib/api/types/autogen/schema.ts" ]; then - echo "Schema generation failed!" - exit 1 - fi - echo "Schema file generated successfully." - - pnpm run dev >../frontend.log 2>&1 & - echo $! >../frontend_pid.txt - cd .. - - frontend_started=false - for i in {1..30}; do - if curl -s http://localhost:5173/ | grep -q ''; then - echo "Frontend is up!" - frontend_started=true - break - fi - echo "Waiting for frontend... ($i/30)" - sleep 2 - done - - if [ "$frontend_started" = false ]; then - echo "Frontend failed to start in time." - cat frontend.log || true - exit 1 - fi -} \ No newline at end of file diff --git a/.github/scripts/run-frontend-tests.sh b/.github/scripts/run-frontend-tests.sh deleted file mode 100644 index 84723f160..000000000 --- a/.github/scripts/run-frontend-tests.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/local-db.sh" -source "$DIR/run-backend-instance.sh" -trap 'backend_cleanup; db_cleanup' EXIT - -db_startup -backend_startup - -corepack enable pnpm -pnpm --dir js i --frozen-lockfile -pnpm --dir js run generate -pnpm --dir js run test diff --git a/.github/scripts/test/run-backend-compile-tests.ts b/.github/scripts/test/run-backend-compile-tests.ts new file mode 100644 index 000000000..a2aad8a60 --- /dev/null +++ b/.github/scripts/test/run-backend-compile-tests.ts @@ -0,0 +1,21 @@ +import { $ } from "bun"; + +async function main() { + // fmt + await $`./mvnw spotless:check`; + + // lint + await $`./mvnw checkstyle:check`; + + // compile + await $`./mvnw -B verify -Dmaven.test.skip=true --no-transfer-progress`; +} + +main() + .then(() => { + process.exit(); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/test/run-backend-tests.ts b/.github/scripts/test/run-backend-tests.ts new file mode 100644 index 000000000..04b249478 --- /dev/null +++ b/.github/scripts/test/run-backend-tests.ts @@ -0,0 +1,41 @@ +import { $ } from "bun"; +import { getEnvVariables } from "load-secrets/env/load"; +import { db } from "utils/run-local-db"; + +async function main() { + try { + const ciAppEnv = await getEnvVariables(["ci-app"]); + const localDbEnv = await db.start(); + + if (!localDbEnv) { + throw new Error("Local db empty when it should not be"); + } + + const $$ = $.env({ + ...process.env, + ...Object.fromEntries(ciAppEnv), + ...localDbEnv, + }); + + await $`./mvnw -B install -D skipTests --no-transfer-progress`; + + await $`./mvnw -B exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install-deps"`; + await $`./mvnw -B exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install firefox"`; + + await $`corepack enable pnpm`; + await $`cd email && pnpm i --frozen-lockfile && ./email.sh && cd ..`; + + await $$`./mvnw clean verify -Dspring.profiles.active=ci`; + } finally { + await db.end(); + } +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/test/run-frontend-tests.ts b/.github/scripts/test/run-frontend-tests.ts new file mode 100644 index 000000000..7e0b42226 --- /dev/null +++ b/.github/scripts/test/run-frontend-tests.ts @@ -0,0 +1,35 @@ +import { $ } from "bun"; +import { getEnvVariables } from "load-secrets/env/load"; +import { backend } from "utils/run-backend-instance"; +import { db } from "utils/run-local-db"; + +async function main() { + try { + const ciAppEnv = await getEnvVariables(["ci-app"]); + const localDbEnv = await db.start(); + await backend.start(ciAppEnv); + + const $$ = $.env({ + ...process.env, + ...Object.fromEntries(ciAppEnv), + ...localDbEnv, + }); + + await $`corepack enable pnpm`; + await $`pnpm --dir js i --frozen-lockfile`; + await $$`pnpm --dir js run generate`; + await $`pnpm --dir js run test`; + } finally { + await backend.end(); + await db.end(); + } +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/transform-yaml-with-env.sh b/.github/scripts/transform-yaml-with-env.sh deleted file mode 100644 index 34e8343f3..000000000 --- a/.github/scripts/transform-yaml-with-env.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -APP_SPEC_PATH="${1:-.do/prod/app.yml}" -ENV_PATH="${2:-.env}" -TMP_ENVS_YAML="$(mktemp 2>/dev/null || echo "/tmp/tmp_envs_$$.yml")" -if [[ -z "$TMP_ENVS_YAML" ]]; then - echo "Failed to create temp file" >&2 - exit 1 -fi -echo "Parsing environment variables..." -: >"$TMP_ENVS_YAML" - -while IFS='=' read -r key val; do - # Skip empty lines or comments - [[ -z "$key" || "$key" =~ ^# ]] && continue - key=$(echo "$key" | xargs) - val=$(echo "$val" | xargs) - printf " - key: %s\n value: \"%s\"\n scope: RUN_AND_BUILD_TIME\n type: SECRET\n" "$key" "$val" >>"$TMP_ENVS_YAML" -done <"$ENV_PATH" - -echo "Replacing root .envs in $APP_SPEC_PATH ..." -yq -i '.envs = load("'"$TMP_ENVS_YAML"'")' "$APP_SPEC_PATH" - -echo "Updated $APP_SPEC_PATH with $(grep -c 'key:' "$TMP_ENVS_YAML") env vars." -rm -f "$TMP_ENVS_YAML" diff --git a/.github/scripts/tsconfig.json b/.github/scripts/tsconfig.json new file mode 100644 index 000000000..82f5612a8 --- /dev/null +++ b/.github/scripts/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "baseUrl": "./" + } +} diff --git a/.github/scripts/types.ts b/.github/scripts/types.ts new file mode 100644 index 000000000..32a48ba5b --- /dev/null +++ b/.github/scripts/types.ts @@ -0,0 +1 @@ +export type Environment = "staging" | "production"; diff --git a/.github/scripts/utils/colors.ts b/.github/scripts/utils/colors.ts new file mode 100644 index 000000000..aebe42cbd --- /dev/null +++ b/.github/scripts/utils/colors.ts @@ -0,0 +1,103 @@ +export function black(s: string) { + return `${BLACK}${s}${RESET}`; +} + +export function red(s: string) { + return `${RED}${s}${RESET}`; +} + +export function green(s: string) { + return `${GREEN}${s}${RESET}`; +} + +export function yellow(s: string) { + return `${YELLOW}${s}${RESET}`; +} + +export function blue(s: string) { + return `${BLUE}${s}${RESET}`; +} + +export function magenta(s: string) { + return `${MAGENTA}${s}${RESET}`; +} + +export function cyan(s: string) { + return `${CYAN}${s}${RESET}`; +} + +export function white(s: string) { + return `${WHITE}${s}${RESET}`; +} + +export function gray(s: string) { + return `${GRAY}${s}${RESET}`; +} + +export function brightRed(s: string) { + return `${BRIGHT_RED}${s}${RESET}`; +} + +export function brightGreen(s: string) { + return `${BRIGHT_GREEN}${s}${RESET}`; +} + +export function brightYellow(s: string) { + return `${BRIGHT_YELLOW}${s}${RESET}`; +} + +export function brightBlue(s: string) { + return `${BRIGHT_BLUE}${s}${RESET}`; +} + +export function brightMagenta(s: string) { + return `${BRIGHT_MAGENTA}${s}${RESET}`; +} + +export function brightCyan(s: string) { + return `${BRIGHT_CYAN}${s}${RESET}`; +} + +export function brightWhite(s: string) { + return `${BRIGHT_WHITE}${s}${RESET}`; +} + +export function bold(s: string) { + return `${BOLD}${s}${RESET}`; +} + +export function dim(s: string) { + return `${DIM}${s}${RESET}`; +} + +export function italic(s: string) { + return `${ITALIC}${s}${RESET}`; +} + +export function underline(s: string) { + return `${UNDERLINE}${s}${RESET}`; +} + +const BLACK = "\x1b[30m"; +const RED = "\x1b[31m"; +const GREEN = "\x1b[32m"; +const YELLOW = "\x1b[33m"; +const BLUE = "\x1b[34m"; +const MAGENTA = "\x1b[35m"; +const CYAN = "\x1b[36m"; +const WHITE = "\x1b[37m"; +const GRAY = "\x1b[90m"; + +const BRIGHT_RED = "\x1b[91m"; +const BRIGHT_GREEN = "\x1b[92m"; +const BRIGHT_YELLOW = "\x1b[93m"; +const BRIGHT_BLUE = "\x1b[94m"; +const BRIGHT_MAGENTA = "\x1b[95m"; +const BRIGHT_CYAN = "\x1b[96m"; +const BRIGHT_WHITE = "\x1b[97m"; + +const RESET = "\x1b[0m"; +const BOLD = "\x1b[1m"; +const DIM = "\x1b[2m"; +const ITALIC = "\x1b[3m"; +const UNDERLINE = "\x1b[4m"; diff --git a/.github/scripts/utils/run-backend-instance.ts b/.github/scripts/utils/run-backend-instance.ts new file mode 100644 index 000000000..505e816da --- /dev/null +++ b/.github/scripts/utils/run-backend-instance.ts @@ -0,0 +1,78 @@ +import { $ } from "bun"; + +import { cyan } from "@/../utils/colors"; + +let be: Bun.Subprocess<"ignore", Bun.BunFile, "inherit"> | undefined; + +async function start(env: Map<string, string>) { + try { + console.log("Starting backend instance..."); + + await $`java -version`; + await $`javac -version`; + console.log(`JAVA_HOME=${process.env.JAVA_HOME}`); + + const logFile = Bun.file("backend.log"); + be = Bun.spawn( + ["./mvnw", "-Dspring-boot.run.profiles=ci", "spring-boot:run"], + { + env: { ...process.env, ...Object.fromEntries(env) }, + stdout: logFile, + }, + ); + + console.log("Waiting for backend to become ready."); + + let ready = false; + const attempts = 30; + + for (let i = 1; i <= attempts; i++) { + try { + const response = await fetch("http://localhost:8080/api"); + const data = (await response.json()) as { success: boolean }; + + if (data.success === true) { + console.log("Backend is up!"); + ready = true; + break; + } + } catch (_) { + /* empty */ + } + + console.log(`Waiting for backend... (${i}/${attempts})`); + await Bun.sleep(2000); + } + + if (!ready) { + console.error("Backend failed to start in time."); + await end(); + process.exit(1); + } + + console.log("backend ready"); + } catch (e) { + console.error(e); + end(); + } +} + +async function end() { + if (be) { + if (!be.killed) { + be.kill(); + } + console.log(cyan("=== BACKEND LOGS ===")); + const logs = await Bun.file("backend.log").text(); + logs + .split("\n") + .filter((s) => s.length > 0) + .forEach((line) => console.log(cyan(line))); + console.log(cyan("=== BACKEND LOGS END ===")); + } +} + +export const backend = { + start, + end, +}; diff --git a/.github/scripts/utils/run-local-db.ts b/.github/scripts/utils/run-local-db.ts new file mode 100644 index 000000000..0332d3589 --- /dev/null +++ b/.github/scripts/utils/run-local-db.ts @@ -0,0 +1,90 @@ +import { $ } from "bun"; + +import { brightMagenta } from "@/../utils/colors"; + +async function start() { + try { + console.log("Starting postgres container..."); + + await $`docker rm -f codebloom-db`; + + await $`docker run -d \ + --name codebloom-db \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=codebloom \ + -p 5440:5432 \ + postgres:16`; + + console.log("Waiting for postgres to become ready."); + + let ready = false; + const attempts = 30; + + for (let i = 1; i <= attempts; i++) { + const check = await $`docker exec codebloom-db pg_isready -U postgres` + .quiet() + .nothrow(); + + if (check.exitCode === 0) { + console.log("postgres is ready!"); + ready = true; + break; + } + + console.log(`Waiting for backend... (${i}/${attempts})`); + await Bun.sleep(2000); + } + + if (!ready) { + console.error("postgres failed to start in time."); + await end(); + process.exit(1); + } + + const env = { + DATABASE_HOST: "localhost", + DATABASE_PORT: "5440", + DATABASE_NAME: "codebloom", + DATABASE_USER: "postgres", + DATABASE_PASSWORD: "postgres", + }; + + console.log("postres started, running migrations..."); + + await $.env(env)`./mvnw flyway:migrate -Dflyway.locations=filesystem:./db`; + + console.log("postgres ready"); + + return env; + } catch (e) { + console.error(e); + end(); + } +} + +async function end() { + console.log("Stopping and removing postgres container..."); + + console.log(brightMagenta("=== DB LOGS ===")); + const logs = await $`docker logs codebloom-db`.text(); + logs + .split("\n") + .filter((s) => s.length > 0) + .forEach((line) => console.log(brightMagenta(line))); + console.log(brightMagenta("=== DB LOGS END ===")); + + await $`docker stop codebloom-db`.quiet().nothrow(); + await $`docker rm codebloom-db`.quiet().nothrow(); + + delete process.env.DATABASE_HOST; + delete process.env.DATABASE_PORT; + delete process.env.DATABASE_NAME; + delete process.env.DATABASE_USER; + delete process.env.DATABASE_PASSWORD; +} + +export const db = { + start, + end, +}; diff --git a/.github/scripts/utils/send-message/gha.ts b/.github/scripts/utils/send-message/gha.ts new file mode 100644 index 000000000..5b41e015f --- /dev/null +++ b/.github/scripts/utils/send-message/gha.ts @@ -0,0 +1,37 @@ +import { sendMessage } from "utils/send-message"; + +const prId = (() => { + const v = process.env.PR_ID; + if (!v) { + throw new Error("PR_ID is required"); + } + const n = Number(v); + if (Number.isNaN(n)) { + throw new Error("PR_ID must be a number"); + } + return n; +})(); + +const message = (() => { + const v = process.env.MESSAGE; + if (!v) { + throw new Error("MESSAGE is required"); + } + return v; +})(); + +/** + * @deprecated usable but is not recommended. + */ +export async function main() { + await sendMessage(prId, message); +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/scripts/utils/send-message/index.ts b/.github/scripts/utils/send-message/index.ts new file mode 100644 index 000000000..7572db8bd --- /dev/null +++ b/.github/scripts/utils/send-message/index.ts @@ -0,0 +1,51 @@ +import { Octokit } from "@octokit/rest"; +import { RequestError } from "octokit"; + +const githubToken = (() => { + const v = process.env.GH_TOKEN; + return v; +})(); + +const [owner, repo] = (() => { + const v = process.env.GITHUB_REPOSITORY; + if (!v) { + throw new Error("GITHUB_REPOSITORY is required"); + } + return v.split("/") as [string, string]; +})(); + +export async function sendMessage( + prId: number, + message: string, + token?: string, +) { + try { + if (!token && !githubToken) { + throw new Error("Some token should be set"); + } + + const client = new Octokit({ + auth: token ?? githubToken, + }); + + try { + await client.rest.issues.createComment({ + issue_number: prId, + owner, + repo, + body: message, + }); + } catch (e) { + let s: string; + if (e instanceof RequestError) { + s = JSON.stringify(e.response?.data); + } else { + s = String(e); + } + throw new Error(`GitHub API Error\n\n${s}`); + } + } catch (e) { + console.error("Failed to post GitHub error message\n", e); + process.exit(1); + } +} diff --git a/.github/scripts/validate-db/index.ts b/.github/scripts/validate-db/index.ts new file mode 100644 index 000000000..7e4970600 --- /dev/null +++ b/.github/scripts/validate-db/index.ts @@ -0,0 +1,46 @@ +import type { Environment } from "types"; + +import { $ } from "bun"; +import { getEnvVariables } from "load-secrets/env/load"; + +const environment: Environment = (() => { + const v = process.env.ENVIRONMENT; + if (!v) { + throw new Error("Environment is required"); + } + + if (v !== "staging" && v !== "production") { + throw new Error( + 'Environment must be the string literal "staging" or "production"', + ); + } + + return v; +})(); + +export async function main() { + // make sure you checkout the repo first. + + const appEnv = await getEnvVariables([ + environment === "staging" ? "staging" : "production-ro", + ]); + + const onlyCheckPendingMigrationFlag = + environment === "staging" ? + "-Dflyway.ignoreMigrationPatterns=*:pending" + : ""; + + await $.env({ + ...process.env, + ...Object.fromEntries(appEnv), + })`./mvnw flyway:validate -Dflyway.locations=filesystem:./db/migration ${onlyCheckPendingMigrationFlag}`; +} + +main() + .then(() => { + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/workflows/ai-command.yml b/.github/workflows/ai-command.yml index baa19d7fe..52aaa179b 100644 --- a/.github/workflows/ai-command.yml +++ b/.github/workflows/ai-command.yml @@ -1,22 +1,65 @@ name: AI Command Handler run-name: Triggering AI review commands on PR #${{ github.event.client_payload.slash_command.args.named.prId }} +concurrency: + group: ai-command-${{ github.event.client_payload.slash_command.args.named.prId || github.event.inputs.prId }} + cancel-in-progress: true + on: repository_dispatch: types: [ai-command] + # this should only be used for testing purposes. + workflow_dispatch: + inputs: + prId: + description: "PR to deploy" + required: true permissions: contents: read pull-requests: write jobs: + getPRHead: + name: Get PR head SHA + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.pr-head.outputs.result }} + steps: + - name: Get PR head SHA + id: pr-head + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const prId = ${{ + (github.event_name == 'repository_dispatch' && github.event.client_payload.slash_command.args.named.prId) + || github.event.inputs.prId + }}; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: parseInt(prId, 10) + }); + return pr.head.sha; + triggerAiReview: name: Trigger AI Review Commands runs-on: ubuntu-latest + needs: getPRHead + steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + ref: ${{ needs.getPRHead.outputs.sha }} + + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - name: Load secrets uses: ./.github/composite/load-secrets @@ -28,20 +71,29 @@ jobs: - name: Post /review command uses: ./.github/composite/send-message with: - prId: ${{ github.event.client_payload.slash_command.args.named.prId }} + prId: ${{ + (github.event_name == 'repository_dispatch' && github.event.client_payload.slash_command.args.named.prId) + || github.event.inputs.prId + }} message: "/review" token: ${{ env.GH_PAT }} - name: Post /describe command uses: ./.github/composite/send-message with: - prId: ${{ github.event.client_payload.slash_command.args.named.prId }} + prId: ${{ + (github.event_name == 'repository_dispatch' && github.event.client_payload.slash_command.args.named.prId) + || github.event.inputs.prId + }} message: "/describe" token: ${{ env.GH_PAT }} - name: Post /improve command uses: ./.github/composite/send-message with: - prId: ${{ github.event.client_payload.slash_command.args.named.prId }} + prId: ${{ + (github.event_name == 'repository_dispatch' && github.event.client_payload.slash_command.args.named.prId) + || github.event.inputs.prId + }} message: "/improve" token: ${{ env.GH_PAT }} diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 800956041..67b6ddf43 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -37,7 +37,7 @@ jobs: repo: context.repo.repo, pull_number: parseInt(prId, 10) }); - return pr.head.sha; + return pr.head.sha; aiCodeReview: name: Run AI Code Review on the PR @@ -50,30 +50,12 @@ jobs: with: ref: ${{ needs.getPRHead.outputs.sha }} - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Setup composite workflow + uses: ./.github/composite/notion-checks with: + PR_ID: ${{ needs.getPRHead.outputs.sha }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci - - - name: Run 'Check Notion PR' composite action - uses: ./.github/composite/check-notion-pr - id: notion_check - with: - PR_ID: ${{ needs.getPRHead.outputs.prId }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - - - name: Echo Notion context - run: | - echo "Context: ${{ steps.notion_check.outputs.context }}" - - - name: Run 'Check Notion Commits' composite action - uses: ./.github/composite/check-notion-commits - with: - PR_ID: ${{ needs.getPRHead.outputs.prId }} - NOTION_ID: ${{ steps.notion_check.outputs.notion_id }} - name: Qodo PR-Agent uses: qodo-ai/pr-agent@main @@ -103,6 +85,9 @@ jobs: pr_reviewer.enable_review_labels_effort: "false" pr_reviewer.final_update_message: "false" pr_description.publish_description_as_comment: "true" + pr_description.extra_instructions: | + Below is some additional context for the PR from the connected Notion task: + ${{ steps.notion_check.outputs.context }} pr_description.publish_description_as_comment_persistent: "false" pr_description.publish_labels: "false" pr_description.add_original_user_description: "false" @@ -112,8 +97,23 @@ jobs: pr_code_suggestions.enable_help_text: "false" pr_code_suggestions.enable_chat_text: "false" pr_code_suggestions.persistent_comment: "false" - pr_code_suggestions.max_history_len: "4" + pr_code_suggestions.max_history_len: "10" pr_code_suggestions.publish_output_no_suggestions: "true" + pr_code_suggestions.extra_instructions: | + Scrutinize this PR in the context of the following Notion task: + ${{ steps.notion_check.outputs.context }} + + Additional review guidelines: + - Verify that the implementation matches the acceptance criteria from the Notion task + - Check for proper error handling and edge cases + - Ensure code follows existing patterns and architecture in the codebase + - Look for potential security vulnerabilities or data validation issues + - Verify that database migrations (if any) are reversible and safe + - Check for proper logging and monitoring considerations + - Ensure API endpoints follow RESTful conventions and existing API patterns + - Verify that environment variables and secrets are handled securely + - Check for potential performance bottlenecks or N+1 query issues + - Ensure proper test coverage for critical paths pr_code_suggestions.apply_suggestions_checkbox: "true" pr_code_suggestions.suggestions_score_threshold: "0" pr_code_suggestions.new_score_mechanism: "true" diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 20c2280f8..ae17f60ff 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -25,7 +25,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Run backend pre test + - name: Run workflow uses: ./.github/composite/test/backend-pre-test backendTests: @@ -37,47 +37,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/test/backend-test with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci,ci-app - - - name: Run script - run: bash .github/scripts/run-backend-tests.sh - - - name: Upload JaCoCo HTML report - uses: actions/upload-artifact@v4 - with: - name: jacoco-report - path: target/site/jacoco/ - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} frontendTests: name: Frontend Tests @@ -88,40 +52,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Fix a bug with corepack by installing corepack globally - run: npm i -g corepack@latest - working-directory: js - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/test/frontend-test with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci-app - - - name: Run script - run: bash .github/scripts/run-frontend-tests.sh testBuildImage: name: Build Test Docker Image @@ -134,35 +69,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Expose GitHub Runtime - uses: crazy-max/ghaction-github-runtime@v3 - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/build-image with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci,ci-app - - - name: Run script - run: bash .github/scripts/build-image.sh - env: + TAG_PREFIX: test- DOCKER_UPLOAD: false validateDBSchema: @@ -174,31 +86,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/validate-db with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: production-ro - - - name: Validate DB Schema - run: ./mvnw flyway:validate -Dflyway.locations=filesystem:./db/migration -Dflyway.ignoreMigrationPatterns="*:pending" buildImage: name: Build Docker Image & Upload to Registry @@ -211,34 +103,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/build-image with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci,ci-app - - name: Expose GitHub Runtime - uses: crazy-max/ghaction-github-runtime@v3 - - - name: Run script - run: bash .github/scripts/build-image.sh redeploy: name: Redeploy on DigitalOcean @@ -252,84 +122,8 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/redeploy with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci,production - - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ env.DIGITAL_OCEAN_PAT }} - - - name: Migrate Production DB - run: ./mvnw flyway:migrate -Dflyway.locations=filesystem:./db/migration - - - name: Transform App Spec with secrets - run: bash .github/scripts/transform-yaml-with-env.sh .do/prod/app.yml .env.production - - - name: Update App Spec on DigitalOcean - run: | - OUTPUT=$(doctl apps update ${{ env.DIGITAL_OCEAN_APP_ID }} --spec .do/prod/app.yml --update-sources --output json) - - DEPLOYMENT_ID=$(echo "$OUTPUT" | jq -r '.[0].pending_deployment.id // .[0].in_progress_deployment.id // .[0].active_deployment.id // empty' | head -n 1) - - if [ -z "$DEPLOYMENT_ID" ] || [ "$DEPLOYMENT_ID" == "null" ]; then - echo "Failed to extract deployment ID from app update response" - exit 1 - fi - - echo "DEPLOYMENT_ID=$DEPLOYMENT_ID" >> $GITHUB_ENV - - - name: Poll Deployment Status - run: | - - if [ -z "$DEPLOYMENT_ID" ]; then - echo "Deployment ID is empty." - exit 1 - fi - - echo "Waiting for deployment to be promoted." - - for i in {1..60}; do - RESPONSE=$(curl -s -X GET \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${{ env.DIGITAL_OCEAN_PAT }}" \ - "https://api.digitalocean.com/v2/apps/${{ env.DIGITAL_OCEAN_APP_ID }}/deployments/$DEPLOYMENT_ID") - - PHASE=$(echo "$RESPONSE" | jq -r '.deployment.phase') - - echo "Deployment phase: $PHASE" - - if [ "$PHASE" == "ACTIVE" ]; then - echo "Deployment is active." - exit 0 - elif [ "$PHASE" == "SUPERSEDED" ] || [ "$PHASE" == "ERROR" ] || [ "$PHASE" == "CANCELED" ]; then - echo "Deployment failed with phase: $PHASE" - exit 1 - fi - - echo "Waiting for deployment to complete, sleep 10... ($i/60)" - sleep 10 - done - - echo "Deployment did not reach a valid state within 10 minutes." - exit 1 diff --git a/.github/workflows/deploy-stg.yml b/.github/workflows/deploy-stg.yml index ebfd27479..87383eabc 100644 --- a/.github/workflows/deploy-stg.yml +++ b/.github/workflows/deploy-stg.yml @@ -105,6 +105,12 @@ jobs: with: ref: ${{ needs.getPRHead.outputs.sha }} + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + - name: Load secrets uses: ./.github/composite/load-secrets with: @@ -139,7 +145,7 @@ jobs: with: ref: ${{ needs.getPRHead.outputs.sha }} - - name: Run backend pre test + - name: Run workflow uses: ./.github/composite/test/backend-pre-test backendTests: @@ -154,36 +160,11 @@ jobs: with: ref: ${{ needs.getPRHead.outputs.sha }} - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets - with: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci-app - - - name: Run script - run: bash .github/scripts/run-backend-tests.sh + # - name: Run workflow + # uses: ./.github/composite/test/backend-test + # with: + # GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + # GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} frontendTests: name: Frontend Tests @@ -197,40 +178,11 @@ jobs: with: ref: ${{ needs.getPRHead.outputs.sha }} - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Fix a bug with corepack by installing corepack globally - run: npm i -g corepack@latest - working-directory: js - - - name: Load secrets - uses: ./.github/composite/load-secrets - with: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci-app - - - name: Run script - run: bash .github/scripts/run-frontend-tests.sh + # - name: Run workflow + # uses: ./.github/composite/test/frontend-test + # with: + # GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + # GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} validateDBSchema: name: Validate DB Schema on Staging DB @@ -243,32 +195,14 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ needs.getPRHead.outputs.sha }} + fetch-depth: 0 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/validate-db with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: staging - - - name: Validate DB Schema - run: ./mvnw flyway:validate -Dflyway.locations=filesystem:./db/migration -Dflyway.ignoreMigrationPatterns="*:pending" + ENVIRONMENT: staging buildImage: name: Build Docker Image & Upload to Registry @@ -290,37 +224,13 @@ jobs: with: ref: ${{ needs.getPRHead.outputs.sha }} - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/build-image with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci,ci-app - - - name: Expose GitHub Runtime - uses: crazy-max/ghaction-github-runtime@v3 - - - name: Run script - run: bash .github/scripts/build-image.sh - env: - TAG_PREFIX: staging- - SERVER_PROFILES: stg + TAG_PREFIX: staging- + SERVER_PROFILES: stg redeploy: name: Redeploy on DigitalOcean @@ -333,101 +243,15 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ needs.getPRHead.outputs.sha }} + fetch-depth: 0 - - name: Disable man-db - uses: ./.github/composite/disable-mandb - - - name: Set up OpenJDK 25 - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "25" - cache: "maven" - - - name: Verify Java version - run: | - java -version - javac -version - echo "JAVA_HOME=$JAVA_HOME" - - - name: Load secrets - uses: ./.github/composite/load-secrets + - name: Run workflow + uses: ./.github/composite/redeploy with: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - UNLOAD_ENVIRONMENTS: ci,staging - - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ env.DIGITAL_OCEAN_PAT }} - - - name: Check for DB changes - id: db-changes - run: | - git fetch origin main:main - if git diff --name-only main...${{ needs.getPRHead.outputs.sha }} | grep -q '^db/'; then - echo "changed=true" >> $GITHUB_OUTPUT - echo "DB changes detected" - else - echo "changed=false" >> $GITHUB_OUTPUT - echo "No DB changes detected" - fi - - - name: Migrate Staging DB - if: steps.db-changes.outputs.changed == 'true' - run: ./mvnw flyway:migrate -Dflyway.locations=filesystem:./db/migration - - - name: Transform App Spec with secrets - run: bash .github/scripts/transform-yaml-with-env.sh .do/stg/app.yml .env.staging - - - name: Update App Spec on DigitalOcean - run: | - OUTPUT=$(doctl apps update ${{ env.DIGITAL_OCEAN_APP_ID }} --spec .do/stg/app.yml --update-sources --output json) - - DEPLOYMENT_ID=$(echo "$OUTPUT" | jq -r '.[0].pending_deployment.id // .[0].in_progress_deployment.id // .[0].active_deployment.id // empty' | head -n 1) - - if [ -z "$DEPLOYMENT_ID" ] || [ "$DEPLOYMENT_ID" == "null" ]; then - echo "Failed to extract deployment ID from app update response" - exit 1 - fi - - echo "DEPLOYMENT_ID=$DEPLOYMENT_ID" >> $GITHUB_ENV - - - name: Poll Deployment Status - run: | - - if [ -z "$DEPLOYMENT_ID" ]; then - echo "Deployment ID is empty." - exit 1 - fi - - echo "Waiting for deployment to be promoted." - - for i in {1..60}; do - RESPONSE=$(curl -s -X GET \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${{ env.DIGITAL_OCEAN_PAT }}" \ - "https://api.digitalocean.com/v2/apps/${{ env.DIGITAL_OCEAN_APP_ID }}/deployments/$DEPLOYMENT_ID") - - PHASE=$(echo "$RESPONSE" | jq -r '.deployment.phase') - - echo "Deployment phase: $PHASE" - - if [ "$PHASE" == "ACTIVE" ]; then - echo "Deployment is active." - exit 0 - elif [ "$PHASE" == "SUPERSEDED" ] || [ "$PHASE" == "ERROR" ] || [ "$PHASE" == "CANCELED" ]; then - echo "Deployment failed with phase: $PHASE" - exit 1 - fi - - echo "Waiting for deployment to complete, sleep 10... ($i/60)" - sleep 10 - done - - echo "Deployment did not reach a valid state within 10 minutes." - exit 1 + ENVIRONMENT: staging + SHA: ${{ needs.getPRHead.outputs.sha }} notifyStatus: name: Notify on deployment status diff --git a/.github/workflows/pr-verifications.yml b/.github/workflows/pr-verifications.yml index e3350e0b2..9dc4b9073 100644 --- a/.github/workflows/pr-verifications.yml +++ b/.github/workflows/pr-verifications.yml @@ -22,16 +22,10 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Run 'Check Notion PR' composite action - uses: ./.github/composite/check-notion-pr + - name: Run composite workflow + uses: ./.github/composite/notion-checks id: notion_check with: PR_ID: ${{ github.event.number }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - - - name: Run 'Check Notion Commits' composite action - uses: ./.github/composite/check-notion-commits - with: - PR_ID: ${{ github.event.number }} - NOTION_ID: ${{ steps.notion_check.outputs.notion_id }} diff --git a/.github/workflows/slash.yml b/.github/workflows/slash.yml index dc997afba..dedbe7cbb 100644 --- a/.github/workflows/slash.yml +++ b/.github/workflows/slash.yml @@ -34,6 +34,12 @@ jobs: with: ref: ${{ steps.pr-head.outputs.result }} + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + - name: Load secrets uses: ./.github/composite/load-secrets with: diff --git a/Justfile b/Justfile index c816b146c..ee5c54ce8 100644 --- a/Justfile +++ b/Justfile @@ -97,6 +97,12 @@ devd *args: ci-test-stg pr_name pr_id: gh workflow run .github/workflows/deploy-stg.yml --ref {{pr_name}} --field prId={{pr_id}} +# Triggers a deploy to staging command for the following environment. Must be authenticated on gh. +# NOTE: Triggering staging deployment this way can cause a buildup of tasks (broken concurrency group). +# As such, this should only be used in one branch at a time. +ci-test-ai pr_name pr_id: + gh workflow run .github/workflows/ai-command.yml --ref {{pr_name}} --field prId={{pr_id}} + # Interactive script to add a user's GPG public key so they can decrypt keys git-crypt-add-user: cd scripts && pnpm i && pnpm run git-crypt-add-user && clear && node git-crypt/add-user diff --git a/js/src/app/_component/AboutUs.tsx b/js/src/app/_component/AboutUs.tsx index a8c7af7ac..6c9fb3583 100644 --- a/js/src/app/_component/AboutUs.tsx +++ b/js/src/app/_component/AboutUs.tsx @@ -45,7 +45,7 @@ export default function AboutUs() { size={"xl"} m={"md"} > - Celebrating CodeBloom's 1 Year Anniversary! 🎉 + Greetings from Bun Shell! </Badge> <Title order={3} style={{ color: "#4cffb0", alignItems: "center" }}> Level Up Your Coding with diff --git a/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java b/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java index 0a0178936..6ecb3a20b 100644 --- a/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java +++ b/src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java @@ -163,7 +163,6 @@ public String getCsrf() { } String stealCookieImpl() { - LOCK.writeLock().lock(); try (Playwright playwright = Playwright.create(); Browser browser = playwright .firefox() @@ -273,10 +272,7 @@ String stealCookieImpl() { } else { log.info("Should be authenticated but not authenticated."); } - } finally { - LOCK.writeLock().unlock(); } - return null; } }