Repository navigation
ci: reuse the normal build for an advisory ABI/API check, published safely #2
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
Open
napetrov
wants to merge
46
commits into
master
Choose a base branch
from
abicheck-shadow-integration-clean
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
8bf1823
ci: add ABICheck shadow ABI scan
napetrov 0b98079
ci: require comparable ABICheck shadow evidence
napetrov 24702cc
ci: install CastXML from conda-forge
napetrov 3201533
ci: collect target-specific ABICheck evidence
napetrov 60f7603
ci: retain ABICheck target failures
napetrov d95ba93
ci: record pvxs C++ dialect in scan evidence
napetrov 8fe7884
ci: override CastXML dialect for GCC 13
napetrov 2a26a19
ci: allow complete pvxs shadow scans
napetrov 839b511
ci: stage PVXS public headers for shadow ABI scan
napetrov d2d4c61
ci: harden PVXS ABICheck shadow integration
napetrov 3825f0d
ci: fail closed on invalid ABI evidence
napetrov d0b31ba
ci: separate public and generated PVXS headers
napetrov c53cc09
ci: stage declared PVXS header sources
napetrov b8a557d
ci: escape ABI summary references
napetrov f4f576b
ci: fail closed when staging ABI reports
napetrov c4de3ae
ci: use L2 ABI scan in shadow workflow
napetrov e955819
ci: update ABICheck scanner to latest main
napetrov 07c17aa
ci: use current ABICheck export syntax
napetrov d1023b8
ci: refresh ABICheck main pin
napetrov a5c52a6
ci: correct ABICheck main revision pin
napetrov 6b096a1
ci: reuse the normal build for the ABI check and publish it safely
napetrov f1cc10a
ci: read the snapshot path from the manifest's own snapshot field
napetrov 8ffffa8
ci: fix baseline eligibility, report identity and the incomplete cont…
napetrov 3732bc9
ci: drop the redundant ABI capture failure marker
napetrov 2f9c269
ci: make the explicit baseline overwrite path actually work
napetrov 0beca2b
ci: keep per-target detail reachable, and pin the reviewed publisher
napetrov 51b33f0
ci: repair the workflow file broken by an invalid env context
napetrov 440fa8e
ci: order the sticky comment by the producer run, and refresh the pin
napetrov 65d3f8d
ci: pin the whole integration to the merged abicheck revision
claude d2c3904
ci: hand the generic ABI machinery back to abicheck
claude 22ca9d5
Merge remote-tracking branch 'origin/master' into abicheck-shadow-int…
claude 7143f5d
docs: record the first real-runner validation of the migrated Actions
claude b13f69d
ci: fix four defects the review found in the migrated integration
claude d97e677
ci: repin to pick up the report fix that made the ordering guard work
claude a4965ce
docs: record why the upstream baseline publisher does not fit yet
claude ea4f758
ci: enforce the default-branch restriction the bootstrap only claimed
claude e27eb5f
docs: state that no real baseline comparison has run on this branch
claude 08b895b
ci: verify a baseline-set's revision before publishing it under a tag
claude 5a129fa
ci: bind the spec as JSON, verify the analysed commit, and say what ran
claude f5ade4a
ci: retest extraction on the CI toolchain, and actually compare somet…
claude 40c8eb8
ci: drop a renderer step that cannot load, and record why
claude bc8e0a1
Merge master into abicheck integration branch
napetrov e4c5428
ci: consolidate the abicheck integration onto supported owners
napetrov daffed4
ci: declare the expected checks independently of comparison execution
napetrov 7cbd1c3
ci: stop pre-creating the baseline channel directories
napetrov d4023bf
ci: repin abicheck to current main and drop default-restating inputs
napetrov 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,32 @@ | ||
| [ | ||
| { | ||
| "name": "libpvxs", | ||
| "artifact": "lib/${EPICS_HOST_ARCH}/libpvxs.so*", | ||
| "header": [ | ||
| "include/pvxs/*.h", | ||
| "include/pvxs/versionNum.h" | ||
| ], | ||
| "header_exclude": [ | ||
| "include/pvxs/iochooks.h" | ||
| ], | ||
| "include": [ | ||
| "include", | ||
| "${EPICS_BASE}/include", | ||
| "${EPICS_BASE}/include/os/Linux", | ||
| "${EPICS_BASE}/include/compiler/gcc" | ||
| ] | ||
| }, | ||
| { | ||
| "name": "libpvxsIoc", | ||
| "artifact": "lib/${EPICS_HOST_ARCH}/libpvxsIoc.so*", | ||
| "header": [ | ||
| "include/pvxs/iochooks.h" | ||
| ], | ||
| "include": [ | ||
| "include", | ||
| "${EPICS_BASE}/include", | ||
| "${EPICS_BASE}/include/os/Linux", | ||
| "${EPICS_BASE}/include/compiler/gcc" | ||
| ] | ||
| } | ||
| ] |
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,25 @@ | ||
| # Extraction context for the advisory abicheck shadow check. | ||
| # | ||
| # This describes how the PUBLIC HEADERS are parsed. It does not change how | ||
| # PVXS itself is built: the libraries analysed are exactly the ones | ||
| # `cue.py build` produced, with PVXS's normal flags. | ||
| compile: | ||
| options: | ||
| # CURRENT LIMITATION. PVXS's supported consumer language mode is C++11, | ||
| # but abicheck's extraction pipeline cannot parse these headers against | ||
| # libstdc++ 13 in C++11 or C++14 (each fails, for a different reason, on | ||
| # the toolchain CI selects). C++17 extraction is therefore a disclosed | ||
| # deviation from the consumer language mode -- it is NOT validation of | ||
| # C++11 consumer behaviour -- and it is applied identically to both | ||
| # components and to both sides of every comparison. Remove it once the | ||
| # pipeline parses the headers in C++11. Measurements and the upstream | ||
| # issue are linked from the pull request. | ||
| - -std=c++17 | ||
| # Deliberately NOT set: PVXS_API_BUILDING (a library-build-only macro) | ||
| # and PVXS_ENABLE_EXPERT_API (an opt-in expert surface). The headers are | ||
| # parsed the way an ordinary consumer sees them. | ||
| assurance: | ||
| # A comparison that could not complete is reported as incomplete, never as | ||
| # a clean result. The shadow gate stays advisory; this controls what the | ||
| # analysis may claim, not whether CI goes red. | ||
| require_complete: true |
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,143 @@ | ||
| name: 'PVXS ABI capture' | ||
| description: >- | ||
| Capture ABI/API evidence for libpvxs and libpvxsIoc from an installation a | ||
| normal PVXS build has already produced. Performs no build, no comparison | ||
| and no reporting. | ||
|
|
||
| What is PVXS-specific lives here: where the EPICS dependency was prepared | ||
| and which host architecture it built for. Selecting the real shared object | ||
| out of its SONAME alias chain, ELF/machine agreement, expanding the owned | ||
| header sets, refusing a stale exclusion and binding the two build-decided | ||
| values into the declaration all belong to abicheck's baseline Action, | ||
| which reads .ci-local/abicheck-components.json. | ||
|
|
||
| Shared by ci-scripts-build.yml's candidate capture and | ||
| abicheck-baseline.yml's historical bootstrap, so the two cannot drift. | ||
|
|
||
| inputs: | ||
| top: | ||
| description: > | ||
| Root of the built PVXS tree to capture from. Defaults to the | ||
| workspace; the historical bootstrap points this at its own checkout. | ||
| required: false | ||
| default: '' | ||
| output-dir: | ||
| description: 'Directory to write the baseline-set into.' | ||
| required: true | ||
| project-ref: | ||
| description: > | ||
| The revision actually built and analysed, recorded in the manifest. | ||
| For a pull_request build this is the merge commit, not the PR head. | ||
| required: true | ||
| profile: | ||
| description: 'Build-profile identifier recorded in the manifest.' | ||
| required: true | ||
| baseline-generation: | ||
| description: 'Scanner-compatibility generation recorded in the manifest.' | ||
| required: false | ||
| default: '1' | ||
| build-config: | ||
| description: 'Path to the abicheck extraction config.' | ||
| required: false | ||
| default: '.ci-local/abicheck.yml' | ||
| component-spec: | ||
| description: 'Path to the PVXS component declaration.' | ||
| required: false | ||
| default: '.ci-local/abicheck-components.json' | ||
| abicheck-ref: | ||
| description: 'Immutable abicheck revision the capture runs from.' | ||
| required: true | ||
|
|
||
| outputs: | ||
| baseline-path: | ||
| description: 'The baseline-set directory that was written.' | ||
| value: ${{ steps.capture.outputs.baseline-path }} | ||
| content-digest: | ||
| description: "Digest over the manifest's artifact list." | ||
| value: ${{ steps.capture.outputs.content-digest }} | ||
| resolved-libraries: | ||
| description: 'The concrete artifact/header/include set that was dumped.' | ||
| value: ${{ steps.capture.outputs.resolved-libraries }} | ||
| machine: | ||
| description: 'The ELF machine both components agreed on (e.g. EM_X86_64).' | ||
| value: ${{ steps.capture.outputs.machine }} | ||
|
|
||
| runs: | ||
| using: 'composite' | ||
| steps: | ||
| - name: Resolve EPICS build context | ||
| id: epics | ||
| shell: bash | ||
| env: | ||
| TOP: ${{ inputs.top || github.workspace }} | ||
| SPEC_IN: ${{ inputs.component-spec }} | ||
| CFG_IN: ${{ inputs.build-config }} | ||
| WORKSPACE: ${{ github.workspace }} | ||
| run: | | ||
| set -eu | ||
|
|
||
| # Both trusted inputs belong to the CHECKOUT, not to the tree being | ||
| # captured: the bootstrap points `top` at a revision that predates | ||
| # them, so a relative path resolved after `cd "$TOP"` would read a | ||
| # missing or stale file from that revision. | ||
| case "$SPEC_IN" in /*) spec=$SPEC_IN ;; *) spec=$WORKSPACE/$SPEC_IN ;; esac | ||
| case "$CFG_IN" in '') cfg= ;; /*) cfg=$CFG_IN ;; *) cfg=$WORKSPACE/$CFG_IN ;; esac | ||
| [ -f "$spec" ] || { echo "::error::component declaration $spec does not exist"; exit 64; } | ||
| if [ -n "$cfg" ] && [ ! -f "$cfg" ]; then | ||
| echo "::error::extraction config $cfg does not exist"; exit 64 | ||
| fi | ||
|
|
||
| cd "$TOP" | ||
|
|
||
| # configure/RELEASE.local is where cue.py records the prepared | ||
| # dependency. We read it; we never rewrite it. | ||
| if [ -z "${EPICS_BASE:-}" ] && [ -f configure/RELEASE.local ]; then | ||
| EPICS_BASE=$(sed -n -E 's/^[[:space:]]*EPICS_BASE[[:space:]]*=[[:space:]]*//p' \ | ||
| configure/RELEASE.local | tail -n 1) | ||
| fi | ||
| [ -n "${EPICS_BASE:-}" ] || { | ||
| echo "::error::EPICS_BASE is not set and configure/RELEASE.local does not name it" | ||
| exit 64 | ||
| } | ||
|
|
||
| EPICS_HOST_ARCH=${EPICS_HOST_ARCH:-} | ||
| if [ -z "$EPICS_HOST_ARCH" ] && [ -x "$EPICS_BASE/startup/EpicsHostArch" ]; then | ||
| EPICS_HOST_ARCH=$("$EPICS_BASE/startup/EpicsHostArch") | ||
| fi | ||
| # This capture is declared for a native Linux install layout only. | ||
| case "$EPICS_HOST_ARCH" in | ||
| linux-*) ;; | ||
| *) echo "::error::expected a native Linux host arch, got '${EPICS_HOST_ARCH:-<unset>}'" | ||
| exit 64 ;; | ||
| esac | ||
|
|
||
| { | ||
| echo "epics-base=$EPICS_BASE" | ||
| echo "host-arch=$EPICS_HOST_ARCH" | ||
| echo "spec=$spec" | ||
| echo "build-config=$cfg" | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Capture ABI snapshots (libpvxs, libpvxsIoc) | ||
| id: capture | ||
| uses: abicheck/abicheck/actions/baseline@902ee996795dbe529a5d6d348a220864634c0e2b | ||
| with: | ||
| library-spec: ${{ steps.epics.outputs.spec }} | ||
| # Binding is abicheck's: it walks the document as JSON and inserts | ||
| # each value literally, so a path containing `&`, a quote or a | ||
| # backslash is not mangled the way a textual pass would. The two | ||
| # names below are the whole allowlist; any other ${...} is refused. | ||
| library-spec-bindings: | | ||
| EPICS_BASE=${{ steps.epics.outputs.epics-base }} | ||
| EPICS_HOST_ARCH=${{ steps.epics.outputs.host-arch }} | ||
| library-root: ${{ inputs.top || github.workspace }} | ||
| output-dir: ${{ inputs.output-dir }} | ||
| project-ref: ${{ inputs.project-ref }} | ||
| profile: ${{ inputs.profile }} | ||
| depth: headers | ||
| build-config: ${{ steps.epics.outputs.build-config }} | ||
| baseline-generation: ${{ inputs.baseline-generation }} | ||
| generator-action-ref: ${{ inputs.abicheck-ref }} | ||
| # A libpvxs snapshot is ~124 MB of JSON at this depth (measured). | ||
| # The decoded content is identical either way. | ||
| snapshot-compression: zstd | ||
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,196 @@ | ||
| # Publish the release-contract ABI baseline-set for the selected profile. | ||
| # | ||
| # Two paths, both running only from this repository's default branch, and | ||
| # both publishing through abicheck's own reusable publish-baseline.yml -- | ||
| # which owns packaging, the manifest/schema/profile/generation/digest gate, | ||
| # tag resolution and the immutability chain (identity-verified idempotent | ||
| # republish, fail-closed on a conflicting asset). | ||
| # | ||
| # automatic A tag build of "PVXS EPICS" already captured the tagged | ||
| # revision. Its capture is published from that run. Nothing | ||
| # is rebuilt. | ||
| # | ||
| # bootstrap A historical release predates the capture integration, so no | ||
| # tag build of it ever produced a baseline-set, and dispatching | ||
| # the old workflow cannot help: the workflow AT that tag has no | ||
| # capture step. This path builds the requested revision once, | ||
| # here, with the current trusted tooling. One-time per release, | ||
| # never part of a pull request. | ||
| # | ||
| # The accepted-main channel needs nothing here: a pull request resolves it | ||
| # from the successful default-branch run for its own base commit. | ||
|
|
||
| name: ABI baseline | ||
|
|
||
| on: | ||
| workflow_run: | ||
| workflows: ["PVXS EPICS"] | ||
| types: [completed] | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: 'Release tag to build, capture and publish a baseline-set for.' | ||
| required: true | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| ABICHECK_PROFILE: linux-x86_64-gcc-default-base7.0-bundled-libevent | ||
| ABICHECK_REF: 902ee996795dbe529a5d6d348a220864634c0e2b | ||
|
|
||
| jobs: | ||
| # ── Automatic: publish the tag build's own capture ───────────────────── | ||
| # | ||
| # A push run of "PVXS EPICS" can be a branch push as easily as a tag push, | ||
| # so ask the shared verifier whether the ref really is a tag naming the | ||
| # commit that was built, and publish only then. PVXS tags are bare | ||
| # versions ("1.5.2"); the verifier assumes no "v" prefix, resolves | ||
| # refs/tags/<name> explicitly rather than through a revision endpoint that | ||
| # would resolve a branch, and peels an annotated tag. | ||
| tag-gate: | ||
| if: >- | ||
| github.event_name == 'workflow_run' && | ||
| github.event.workflow_run.event == 'push' && | ||
| github.event.workflow_run.conclusion == 'success' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: read | ||
| outputs: | ||
| eligible: ${{ steps.tag.outputs.eligible }} | ||
| steps: | ||
| - id: tag | ||
| uses: abicheck/abicheck/actions/verify-baseline-source@902ee996795dbe529a5d6d348a220864634c0e2b | ||
| with: | ||
| mode: tag | ||
| tag: ${{ github.event.workflow_run.head_branch }} | ||
| built-sha: ${{ github.event.workflow_run.head_sha }} | ||
| - if: ${{ steps.tag.outputs.eligible != 'true' }} | ||
| env: | ||
| OUTCOME: ${{ steps.tag.outputs.outcome }} | ||
| run: | | ||
| echo "nothing to publish from this run; tag outcome: $OUTCOME" | ||
|
|
||
| publish: | ||
| needs: tag-gate | ||
| if: ${{ needs.tag-gate.outputs.eligible == 'true' }} | ||
| permissions: | ||
| # actions: read is what takes the artifact bytes through the API from | ||
| # the producer run this publication was triggered by. | ||
| actions: read | ||
| contents: write | ||
| uses: abicheck/abicheck/.github/workflows/publish-baseline.yml@902ee996795dbe529a5d6d348a220864634c0e2b | ||
| with: | ||
| # Publish the finished set the producer captured; no dump, no build | ||
| # query, no compiler, no comparison happens in this workflow. | ||
| baseline-set-artifact-prefix: abicheck-candidate- | ||
| # The set comes from a DIFFERENT, already-completed run, so its origin | ||
| # is established rather than assumed: repository, workflow identity, | ||
| # event, id, attempt and conclusion are each checked, the artifacts are | ||
| # then fetched by their own ids, and a pull-request-triggered producer | ||
| # is refused unconditionally. | ||
| baseline-set-source-run-id: ${{ github.event.workflow_run.id }} | ||
| baseline-set-source-repository: ${{ github.repository }} | ||
| baseline-set-source-run-attempt: ${{ github.event.workflow_run.run_attempt }} | ||
| baseline-set-expect-workflow: .github/workflows/ci-scripts-build.yml | ||
| baseline-set-expect-event: push | ||
| baseline-set-allowed-conclusions: success | ||
| release-tag: ${{ github.event.workflow_run.head_branch }} | ||
| # The publication tag and the revision the set records are two | ||
| # different identifiers: a producer stamps the commit it built. | ||
| # 'commit' resolves the tag through refs/tags/<name> and requires the | ||
| # set's own project_ref to equal that commit -- never the other way | ||
| # round, and never a tag-valued rewrite of a SHA-valued manifest. | ||
| expected-project-ref: commit | ||
| baseline-generation: '1' | ||
| asset-name-template: 'abicheck-baseline-{profile}.tar.zst' | ||
|
|
||
| # ── Bootstrap: build a historical release once, here ─────────────────── | ||
| # | ||
| # Split in two on purpose. This job runs the requested revision's own code | ||
| # (cue.py, its submodules, its makefiles) and therefore holds no write | ||
| # scope. It hands the captured set to the publishing job as an artifact. | ||
| bootstrap-build: | ||
| if: ${{ github.event_name == 'workflow_dispatch' }} | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| # The trusted tooling (capture action, component declaration) comes from | ||
| # this checkout; the revision being captured is checked out separately | ||
| # under historical/, and its own workflow is never run. | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Resolve the requested tag | ||
| id: tag | ||
| uses: abicheck/abicheck/actions/verify-baseline-source@902ee996795dbe529a5d6d348a220864634c0e2b | ||
| with: | ||
| mode: tag | ||
| tag: ${{ inputs.tag }} | ||
|
|
||
| - name: Check out the historical revision | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ steps.tag.outputs.commit-sha }} | ||
| submodules: true | ||
| persist-credentials: false | ||
| path: historical | ||
|
|
||
| - name: apt-get install | ||
| run: | | ||
| sudo apt-get update | ||
| sudo apt-get -y install libreadline-dev cmake | ||
|
|
||
| # PVXS's normal build commands, the same ones the CI matrix runs. | ||
| - name: Build the historical revision | ||
| working-directory: historical | ||
| run: | | ||
| set -eu | ||
| python .ci/cue.py prepare | ||
| python .ci/cue.py exec python .ci-local/libevent.py | ||
| python .ci/cue.py build | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| # Same declaration and same shared action as the matrix leg, so the | ||
| # bootstrap cannot drift from normal capture. | ||
| - name: Capture the historical revision | ||
| uses: ./.github/actions/abicheck-capture | ||
| with: | ||
| top: ${{ github.workspace }}/historical | ||
| output-dir: ${{ runner.temp }}/baseline-set | ||
| project-ref: ${{ steps.tag.outputs.commit-sha }} | ||
| profile: ${{ env.ABICHECK_PROFILE }} | ||
| abicheck-ref: ${{ env.ABICHECK_REF }} | ||
|
|
||
| # An artifact, not a path: the set has to cross a real job boundary, and | ||
| # a producer-local directory does not exist in the publishing job. | ||
| - name: Hand the baseline-set to the publishing job | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: abicheck-bootstrap-${{ env.ABICHECK_PROFILE }} | ||
| path: ${{ runner.temp }}/baseline-set | ||
| if-no-files-found: error | ||
| retention-days: 1 | ||
|
|
||
| bootstrap-publish: | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| needs: bootstrap-build | ||
| # workflow_dispatch runs from whatever ref was selected. Refuse to | ||
| # publish off anything but the default branch, so the write-capable half | ||
| # can never be a dispatched branch's copy of this file. | ||
| if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} | ||
| permissions: | ||
| actions: read | ||
| contents: write | ||
| uses: abicheck/abicheck/.github/workflows/publish-baseline.yml@902ee996795dbe529a5d6d348a220864634c0e2b | ||
| with: | ||
| # Same-run handoff: the artifact was uploaded by bootstrap-build, in | ||
| # this run, so no source-run verification is needed or possible. | ||
| baseline-set-artifact-prefix: abicheck-bootstrap- | ||
| release-tag: ${{ inputs.tag }} | ||
| expected-project-ref: commit | ||
| baseline-generation: '1' | ||
| asset-name-template: 'abicheck-baseline-{profile}.tar.zst' | ||
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.