Repository navigation
Add CodeCharta + DependaCharta reusable workflow #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
bd53eae
Add universal-codecharta composite actions and reusable workflow
peuf0u 3b76f4e
Annotate workflow with walk-through comments and fix CI failures
peuf0u 9e569ac
Strip literal `futuredapp/.github/` from top-of-file comment
peuf0u 1708e02
Move data_repo and picker_base_url from hardcoded env to required inputs
peuf0u 3254b03
Revert accidental @2.3.3 → @main flip on unrelated workflows
peuf0u ad60896
Flip internal refs to @ci/codecharta-actions for pre-merge smoke testing
peuf0u c5f5739
Restore internal refs to @main now that smoke test is green
peuf0u fe20c41
Flip internal refs to @ci/codecharta-actions for second smoke test
peuf0u feb2391
Address Copilot review feedback on PR #105
peuf0u 5fd2f1b
Revert accidental @2.3.3 → @main flip on unrelated workflows (again)
peuf0u 8bbb277
Write PR title sidecar on preview path, not only on history
peuf0u 58be6c2
Address second Copilot review: input validation + best-effort fix
peuf0u c834d79
Address third Copilot review
peuf0u cdebf2c
Stop deleting latest.{cg,meta}.json when newest entry lacks them
peuf0u e4f7bf3
Address fifth Copilot review
peuf0u 2fa7d15
Revert pr_limit type back to string
peuf0u 6c9b4ce
Revert tokei --type splitting; use the single-flag comma-list form
peuf0u 0e8a91b
Fix nested-quote bug in history mode field separator
peuf0u 238645e
Expose dependacharta_image_base as a workflow input
peuf0u 5508bf2
Move mkdir into apply_change to fix retry-path failure
peuf0u 2db45dd
Merge remote-tracking branch 'origin/main' into ci/codecharta-actions
ssestak a3256a5
Load the PR graph into DependaCharta via ?file=
ssestak c75984d
Point codecharta action refs at the current release
ssestak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| name: CodeCharta publish | ||
| description: > | ||
| Publishes a CodeCharta snapshot (or removes a preview) into the data repo | ||
| the consumer specifies via `data_repo`. Clones the data repo, copies the | ||
| artifacts into `projects/<project_name>/...`, rebuilds the manifest, and | ||
| pushes back to the data repo's default branch. Handles concurrent commits | ||
| via retry-on-conflict. | ||
|
|
||
| inputs: | ||
| mode: | ||
| required: true | ||
| description: > | ||
| One of: preview, history, bulk-history, delete-preview, set-latest. | ||
| `preview` and `delete-preview` accept a numeric `pr_number`. `history` | ||
| accepts `stem` + `date` + paths. `bulk-history` accepts `snapshots_dir`. | ||
| `set-latest` takes no extra inputs. | ||
| project_name: | ||
| required: true | ||
| description: > | ||
| Output folder name in the data repo (`projects/<project_name>/`). | ||
| data_repo: | ||
| required: true | ||
| description: > | ||
| `<owner>/<name>` of the data repository that holds the published | ||
| snapshots and the manifest. No default — supplied per consumer so this | ||
| public action doesn't bake in a specific (potentially private) repo. | ||
| data_repo_branch: | ||
| required: false | ||
| default: main | ||
| description: > | ||
| Branch in the data repo to push to. | ||
| data_repo_token: | ||
| required: true | ||
| description: > | ||
| Token with write access to the data repo. Use a fine-grained PAT or an | ||
| app token scoped to `Contents: read+write`. | ||
| pr_number: | ||
| required: false | ||
| description: > | ||
| Pull-request number. Required for `preview` and `delete-preview`. | ||
| stem: | ||
| required: false | ||
| description: > | ||
| Snapshot stem, either `pr-<N>` or `merge-<short-sha>`. Required for | ||
| `history`. Not used by `bulk-history` (which discovers stems from the | ||
| snapshot filenames in `snapshots_dir`). | ||
| date: | ||
| required: false | ||
| description: > | ||
| Snapshot date as `YYYY-MM-DD`. Required for `history`. Not used by | ||
| `bulk-history` or `set-latest`. | ||
| cc_path: | ||
| required: false | ||
| description: > | ||
| Absolute path to the local `.cc.json.gz` to publish. Required for | ||
| `preview` and `history`. Not used by `bulk-history` (paths come from | ||
| `snapshots_dir`). | ||
| cg_path: | ||
| required: false | ||
| description: > | ||
| Absolute path to the local `.cg.json` to publish. Required for `preview` | ||
| and `history`. Bulk-history snapshots don't include `.cg.json` so this | ||
| isn't applicable there. | ||
| meta_path: | ||
| required: false | ||
| description: > | ||
| Absolute path to the local `.meta.json` sidecar (PR title/author/etc.). | ||
| Optional; carried through to the data repo when present. | ||
| snapshots_dir: | ||
| required: false | ||
| description: > | ||
| Absolute path to a directory containing historical snapshots named | ||
| `<project>-<YYYY-MM-DD>-(pr-<N>|merge-<sha>).cc.json.gz` (with optional | ||
| `.meta.json` sidecars). Required for `bulk-history`. One clone, idempotent | ||
| copies for entries not yet present, one summary commit, one push. | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Publish to data repo | ||
| shell: bash | ||
| run: ${{ github.action_path }}/codecharta-publish.sh | ||
| env: | ||
| CODECHARTA_PUBLISH_MODE: ${{ inputs.mode }} | ||
| PROJECT_NAME: ${{ inputs.project_name }} | ||
| DATA_REPO: ${{ inputs.data_repo }} | ||
| DATA_REPO_BRANCH: ${{ inputs.data_repo_branch }} | ||
| CODEBASE_ARCHITECTURES_TOKEN: ${{ inputs.data_repo_token }} | ||
| PR_NUMBER: ${{ inputs.pr_number }} | ||
| STEM: ${{ inputs.stem }} | ||
| DATE: ${{ inputs.date }} | ||
| SOURCE_CC_PATH: ${{ inputs.cc_path }} | ||
| SOURCE_CG_PATH: ${{ inputs.cg_path }} | ||
| SOURCE_META_PATH: ${{ inputs.meta_path }} | ||
| SNAPSHOTS_DIR: ${{ inputs.snapshots_dir }} |
297 changes: 297 additions & 0 deletions
297
.github/actions/universal-codecharta-publish/codecharta-publish.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,297 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Publish CodeCharta snapshots to the data repo specified via DATA_REPO env. | ||
| # Invoked by the universal-codecharta-publish composite action. | ||
| # | ||
| # Inputs (env, set by action.yml from action inputs): | ||
| # CODECHARTA_PUBLISH_MODE One of: preview, history, bulk-history, | ||
| # delete-preview, set-latest. | ||
| # PROJECT_NAME Folder name under `projects/`. | ||
| # DATA_REPO `<owner>/<name>` of the data repository. | ||
| # DATA_REPO_BRANCH Branch to push to (default: main). | ||
| # CODEBASE_ARCHITECTURES_TOKEN | ||
| # Write-scoped token for the data repo. | ||
| # PR_NUMBER Pull-request number (preview/delete-preview). | ||
| # STEM `pr-<N>` or `merge-<short-sha>` (history mode). | ||
| # DATE `YYYY-MM-DD` (history mode). | ||
| # SOURCE_CC_PATH Absolute local path to .cc.json.gz to publish. | ||
| # SOURCE_CG_PATH Absolute local path to .cg.json to publish. | ||
| # SOURCE_META_PATH Optional absolute local path to .meta.json. | ||
| # SNAPSHOTS_DIR Absolute path to dir of historical snapshots | ||
| # (bulk-history mode). | ||
|
|
||
| require_env() { | ||
| local name="$1" | ||
| if [[ -z "${!name:-}" ]]; then | ||
| echo "missing required env: ${name}" >&2 | ||
| exit 64 | ||
| fi | ||
| } | ||
|
|
||
| require_env CODECHARTA_PUBLISH_MODE | ||
| require_env PROJECT_NAME | ||
| require_env DATA_REPO | ||
| require_env CODEBASE_ARCHITECTURES_TOKEN | ||
|
|
||
| # Validate PROJECT_NAME — used unquoted in path construction (e.g. | ||
| # "projects/${PROJECT_NAME}/previews/…"). A value like `../foo` would let the | ||
| # script write outside the projects/ tree. Restrict to a safe character set | ||
| # AND reject pure-dot values which the regex accepts but are still traversal: | ||
| # `PROJECT_NAME=..` makes PROJECT_DIR `projects/..` (i.e. the data repo root). | ||
| if [[ ! "${PROJECT_NAME}" =~ ^[A-Za-z0-9._-]+$ ]] || [[ "${PROJECT_NAME}" == "." || "${PROJECT_NAME}" == ".." ]]; then | ||
| echo "PROJECT_NAME must match [A-Za-z0-9._-]+ and not be '.' or '..' (got '${PROJECT_NAME}')" >&2 | ||
| exit 64 | ||
| fi | ||
|
|
||
| # Validate DATA_REPO — interpolated into the clone URL. Shell-quoting prevents | ||
| # command injection, but an unconstrained value can still target unintended | ||
| # repositories. Restrict to <owner>/<name> with the same safe charset. | ||
| if [[ ! "${DATA_REPO}" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then | ||
| echo "DATA_REPO must match <owner>/<name> (got '${DATA_REPO}')" >&2 | ||
| exit 64 | ||
| fi | ||
|
|
||
| DATA_REPO_BRANCH="${DATA_REPO_BRANCH:-main}" | ||
| MODE="${CODECHARTA_PUBLISH_MODE}" | ||
|
|
||
| case "${MODE}" in | ||
| preview | delete-preview) | ||
| require_env PR_NUMBER | ||
| if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then | ||
| echo "${MODE}: PR_NUMBER must be numeric (got '${PR_NUMBER}')" >&2 | ||
| exit 64 | ||
| fi | ||
| ;; | ||
| history) | ||
| require_env STEM | ||
| require_env DATE | ||
| if [[ ! "${STEM}" =~ ^(pr-[0-9]+|merge-[0-9a-f]{7,})$ ]]; then | ||
| echo "${MODE}: STEM must match 'pr-<N>' or 'merge-<sha>' (got '${STEM}')" >&2 | ||
| exit 64 | ||
| fi | ||
| if [[ ! "${DATE}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then | ||
| echo "${MODE}: DATE must be YYYY-MM-DD (got '${DATE}')" >&2 | ||
| exit 64 | ||
| fi | ||
| ;; | ||
| bulk-history) | ||
| require_env SNAPSHOTS_DIR | ||
| if [[ ! -d "${SNAPSHOTS_DIR}" ]]; then | ||
| echo "${MODE}: SNAPSHOTS_DIR must be a directory (got '${SNAPSHOTS_DIR}')" >&2 | ||
| exit 64 | ||
| fi | ||
| ;; | ||
| set-latest) | ||
| # No required args; picks newest history entry by lexicographic sort. | ||
| ;; | ||
| *) | ||
| echo "Unknown CODECHARTA_PUBLISH_MODE: '${MODE}'" >&2 | ||
| echo "Expected one of: preview, history, bulk-history, delete-preview, set-latest" >&2 | ||
| exit 64 | ||
| ;; | ||
| esac | ||
|
|
||
| WORK_DIR="$(mktemp -d)" | ||
| trap 'rm -rf "${WORK_DIR}"' EXIT | ||
|
|
||
| DATA_REPO_NAME="${DATA_REPO##*/}" | ||
| DATA_REPO_DIR="${WORK_DIR}/${DATA_REPO_NAME}" | ||
|
|
||
| echo "Cloning ${DATA_REPO}…" | ||
| git clone --depth=1 --branch "${DATA_REPO_BRANCH}" \ | ||
| "https://x-access-token:${CODEBASE_ARCHITECTURES_TOKEN}@github.com/${DATA_REPO}.git" \ | ||
| "${DATA_REPO_DIR}" | ||
|
|
||
| cd "${DATA_REPO_DIR}" | ||
| git config user.email "codecharta-bot@users.noreply.github.com" | ||
| git config user.name "codecharta-bot" | ||
| git remote set-url origin "https://x-access-token:${CODEBASE_ARCHITECTURES_TOKEN}@github.com/${DATA_REPO}.git" | ||
|
|
||
| PROJECT_DIR="projects/${PROJECT_NAME}" | ||
|
|
||
|
peuf0u marked this conversation as resolved.
|
||
| apply_change() { | ||
| # Re-create the destination directories on every call. Important on the | ||
| # retry path: `git reset --hard origin/<branch>` wipes any local-only | ||
| # directory that the remote winner didn't create. For example, a preview | ||
| # run that wins the race leaves `previews/` but not `history/` in the | ||
| # tree; a losing history run then resets to that state and would `cp` | ||
| # into a missing directory without this mkdir. `mkdir -p` is idempotent, | ||
| # so it costs nothing on the happy path. | ||
| mkdir -p "${PROJECT_DIR}/previews" "${PROJECT_DIR}/history" | ||
| case "${MODE}" in | ||
| preview) | ||
| require_env SOURCE_CC_PATH | ||
| require_env SOURCE_CG_PATH | ||
| cp "${SOURCE_CC_PATH}" "${PROJECT_DIR}/previews/pr-${PR_NUMBER}.cc.json.gz" | ||
| cp "${SOURCE_CG_PATH}" "${PROJECT_DIR}/previews/pr-${PR_NUMBER}.cg.json" | ||
| # Mirror the meta sidecar OR explicitly remove it when source is | ||
| # missing. Without the removal a previous run's stale meta (e.g. | ||
| # an older PR title before a rename) would persist. | ||
| if [[ -n "${SOURCE_META_PATH:-}" && -f "${SOURCE_META_PATH}" ]]; then | ||
| cp "${SOURCE_META_PATH}" "${PROJECT_DIR}/previews/pr-${PR_NUMBER}.meta.json" | ||
|
peuf0u marked this conversation as resolved.
|
||
| else | ||
| rm -f "${PROJECT_DIR}/previews/pr-${PR_NUMBER}.meta.json" | ||
| fi | ||
| ;; | ||
| history) | ||
| require_env SOURCE_CC_PATH | ||
| require_env SOURCE_CG_PATH | ||
| local basename="${DATE}-${STEM}" | ||
| cp "${SOURCE_CC_PATH}" "${PROJECT_DIR}/history/${basename}.cc.json.gz" | ||
| cp "${SOURCE_CG_PATH}" "${PROJECT_DIR}/history/${basename}.cg.json" | ||
| # As with preview: mirror or actively remove. A new history entry | ||
| # with no source meta should NOT inherit a stale per-entry meta | ||
| # from a prior write, and the latest.* pointers below must follow. | ||
| if [[ -n "${SOURCE_META_PATH:-}" && -f "${SOURCE_META_PATH}" ]]; then | ||
| cp "${SOURCE_META_PATH}" "${PROJECT_DIR}/history/${basename}.meta.json" | ||
| else | ||
| rm -f "${PROJECT_DIR}/history/${basename}.meta.json" | ||
| fi | ||
| cp "${PROJECT_DIR}/history/${basename}.cc.json.gz" "${PROJECT_DIR}/history/latest.cc.json.gz" | ||
| cp "${PROJECT_DIR}/history/${basename}.cg.json" "${PROJECT_DIR}/history/latest.cg.json" | ||
| if [[ -f "${PROJECT_DIR}/history/${basename}.meta.json" ]]; then | ||
| cp "${PROJECT_DIR}/history/${basename}.meta.json" "${PROJECT_DIR}/history/latest.meta.json" | ||
|
peuf0u marked this conversation as resolved.
|
||
| else | ||
| rm -f "${PROJECT_DIR}/history/latest.meta.json" | ||
| fi | ||
| ;; | ||
| bulk-history) | ||
| # Iterate over <project>-<date>-<stem>.cc.json.gz files. Idempotent | ||
| # per entry: skip files that already exist in the data repo. | ||
| shopt -s nullglob | ||
| local cc base rest entry_date entry_stem entry_cc entry_meta | ||
| local total=0 | ||
| local copied=0 | ||
| for cc in "${SNAPSHOTS_DIR}/${PROJECT_NAME}"-*.cc.json.gz; do | ||
| total=$(( total + 1 )) | ||
| base="$(basename "${cc}" .cc.json.gz)" | ||
| rest="${base#${PROJECT_NAME}-}" | ||
| if [[ ! "${rest}" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2})-((pr|merge)-[0-9a-f]+)$ ]]; then | ||
| echo "Skipping unrecognised filename: ${base}" >&2 | ||
| continue | ||
| fi | ||
| entry_date="${BASH_REMATCH[1]}" | ||
| entry_stem="${BASH_REMATCH[2]}" | ||
| local entry_basename="${entry_date}-${entry_stem}" | ||
| local entry_target_cc="${PROJECT_DIR}/history/${entry_basename}.cc.json.gz" | ||
| local entry_target_meta="${PROJECT_DIR}/history/${entry_basename}.meta.json" | ||
| entry_cc="${cc}" | ||
| entry_meta="${cc%.cc.json.gz}.meta.json" | ||
|
|
||
| if [[ ! -f "${entry_target_cc}" ]]; then | ||
| cp "${entry_cc}" "${entry_target_cc}" | ||
| copied=$(( copied + 1 )) | ||
| fi | ||
| if [[ -f "${entry_meta}" && ! -f "${entry_target_meta}" ]]; then | ||
| cp "${entry_meta}" "${entry_target_meta}" | ||
| fi | ||
| done | ||
| echo "Bulk-history scanned ${total} entries; wrote ${copied} new history files." | ||
| ;; | ||
| delete-preview) | ||
| rm -f "${PROJECT_DIR}/previews/pr-${PR_NUMBER}.cc.json.gz" \ | ||
| "${PROJECT_DIR}/previews/pr-${PR_NUMBER}.cg.json" \ | ||
| "${PROJECT_DIR}/previews/pr-${PR_NUMBER}.meta.json" | ||
| ;; | ||
| set-latest) | ||
| local newest | ||
| newest=$(ls -1 "${PROJECT_DIR}/history/" 2>/dev/null \ | ||
| | grep -E '\.cc\.json\.gz$' \ | ||
| | grep -v '^latest' \ | ||
| | sort -r \ | ||
| | head -1 || true) | ||
| if [[ -z "${newest}" ]]; then | ||
| echo "No history entries found; nothing to set as latest." | ||
| return | ||
| fi | ||
| local base="${newest%.cc.json.gz}" | ||
| cp "${PROJECT_DIR}/history/${newest}" "${PROJECT_DIR}/history/latest.cc.json.gz" | ||
| # Update the cg/meta sidecars only when the selected newest entry | ||
| # has them. Otherwise leave the existing latest.* alone — backfill | ||
| # entries intentionally omit `.cg.json` (DependaCharta isn't run | ||
| # per historical commit), and we'd rather keep a graph from an | ||
| # older entry than break the picker's DependaCharta view entirely. | ||
| # `latest.meta.json` follows the same policy: rebase-merge history | ||
| # entries have no PR to query, but a prior PR-merge entry's title | ||
| # is more useful than no title at all. | ||
| if [[ -f "${PROJECT_DIR}/history/${base}.cg.json" ]]; then | ||
| cp "${PROJECT_DIR}/history/${base}.cg.json" "${PROJECT_DIR}/history/latest.cg.json" | ||
| fi | ||
| if [[ -f "${PROJECT_DIR}/history/${base}.meta.json" ]]; then | ||
| cp "${PROJECT_DIR}/history/${base}.meta.json" "${PROJECT_DIR}/history/latest.meta.json" | ||
|
peuf0u marked this conversation as resolved.
|
||
| fi | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| apply_change | ||
|
|
||
| if [[ -f scripts/build-manifest.mjs ]]; then | ||
| echo "Rebuilding manifest…" | ||
| node scripts/build-manifest.mjs | ||
| fi | ||
|
|
||
| # Stage everything first so untracked files (new snapshots for a project that | ||
| # didn't exist in the data repo yet) are visible to the diff check. Without | ||
| # this, `git diff --quiet` reports a clean tree because it doesn't look at | ||
| # untracked paths, and we'd exit without publishing the new snapshot. | ||
| git add -A | ||
| if git diff --cached --quiet; then | ||
| echo "No changes after publish; nothing to commit." | ||
| exit 0 | ||
| fi | ||
|
|
||
| case "${MODE}" in | ||
| preview) | ||
| COMMIT_MSG="Update preview for ${PROJECT_NAME} PR #${PR_NUMBER}" | ||
| ;; | ||
| history) | ||
| COMMIT_MSG="Add history snapshot ${PROJECT_NAME} ${DATE}-${STEM}" | ||
| ;; | ||
| bulk-history) | ||
| COMMIT_MSG="Backfill history snapshots for ${PROJECT_NAME}" | ||
| ;; | ||
| delete-preview) | ||
| COMMIT_MSG="Drop preview for ${PROJECT_NAME} PR #${PR_NUMBER}" | ||
| ;; | ||
| set-latest) | ||
| COMMIT_MSG="Refresh latest snapshot pointer for ${PROJECT_NAME}" | ||
| ;; | ||
| esac | ||
|
|
||
| git commit -m "${COMMIT_MSG}" | ||
|
|
||
| # Retry-on-conflict push: instead of `git pull --rebase` (which can fail with | ||
| # a merge conflict on manifest.json under concurrent writers and abort the | ||
| # script via `set -e`), reset hard to remote HEAD and re-apply our changes. | ||
| # This works because: | ||
| # - apply_change is idempotent on each mode (history/bulk-history skip | ||
| # existing entries; preview overwrites; delete-preview is `rm -f`). | ||
| # - manifest.json is rebuilt deterministically from the filesystem. | ||
| # Up to 5 attempts with linear backoff. | ||
| attempt=1 | ||
| while (( attempt <= 5 )); do | ||
| if git push origin "${DATA_REPO_BRANCH}"; then | ||
| echo "Pushed on attempt ${attempt}." | ||
| exit 0 | ||
| fi | ||
| echo "Push rejected (attempt ${attempt}); resetting to remote and reapplying…" | ||
| git fetch origin "${DATA_REPO_BRANCH}" | ||
| git reset --hard "origin/${DATA_REPO_BRANCH}" | ||
| apply_change | ||
| if [[ -f scripts/build-manifest.mjs ]]; then | ||
| node scripts/build-manifest.mjs | ||
| fi | ||
| git add -A | ||
| if git diff --cached --quiet; then | ||
| echo "After reapply, nothing to commit — concurrent writer already covered our changes." | ||
| exit 0 | ||
| fi | ||
| git commit -m "${COMMIT_MSG}" | ||
| sleep $(( attempt * 3 )) | ||
| (( attempt++ )) | ||
| done | ||
|
|
||
| echo "Push failed after 5 attempts; giving up." >&2 | ||
| exit 1 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.