Full Code of burrunan/gradle-cache-action for AI

main 719a4e72b21e cached
124 files
365.4 KB
92.6k tokens
1 requests
Download .txt
Showing preview only (409K chars total). Download the full file or copy to clipboard to get everything.
Repository: burrunan/gradle-cache-action
Branch: main
Commit: 719a4e72b21e
Files: 124
Total size: 365.4 KB

Directory structure:
gitextract_dd0zirf7/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── check-action-typing.yml
│       ├── gradle-wrapper-validation.yml
│       └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── NOTICE
├── README.md
├── action-types.yml
├── action.yml
├── build.gradle.kts
├── cache-action-entrypoint/
│   ├── build.gradle.kts
│   ├── src/
│   │   ├── jsMain/
│   │   │   └── kotlin/
│   │   │       ├── main.kt
│   │   │       └── stringArgv.kt
│   │   └── jsTest/
│   │       └── kotlin/
│   │           └── com/
│   │               └── github/
│   │                   └── burrunan/
│   │                       ├── ArgumentsTest.kt
│   │                       └── SplitLinesTest.kt
│   └── webpack.config.d/
│       └── config.js
├── cache-proxy/
│   ├── build.gradle.kts
│   └── src/
│       ├── jsMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── github/
│       │               └── burrunan/
│       │                   └── gradle/
│       │                       └── proxy/
│       │                           └── CacheProxy.kt
│       └── jsTest/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── gradle/
│                               └── proxy/
│                                   └── CacheProxyTest.kt
├── cache-service-mock/
│   ├── build.gradle.kts
│   └── src/
│       └── jsMain/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── gradle/
│                               └── cache/
│                                   ├── CacheService.kt
│                                   ├── CacheStorage.kt
│                                   ├── HttpException.kt
│                                   └── HttpExtensions.kt
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle-launcher/
│   ├── build.gradle.kts
│   └── src/
│       ├── jsMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── github/
│       │               └── burrunan/
│       │                   └── launcher/
│       │                       ├── GradleDistribution.kt
│       │                       ├── GradleInstaller.kt
│       │                       ├── GradleLauncher.kt
│       │                       ├── GradleVersion.kt
│       │                       ├── GradleVersionResponse.kt
│       │                       ├── LaunchParams.kt
│       │                       └── internal/
│       │                           ├── GradleError.kt
│       │                           ├── GradleErrorCollector.kt
│       │                           └── GradleOutErrorCollector.kt
│       └── jsTest/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── launcher/
│                               ├── PropertiesParserTest.kt
│                               ├── RetrieveGradleVersionTest.kt
│                               └── internal/
│                                   ├── GradleErrorCollectorTest.kt
│                                   └── GradleOutCollectorTest.kt
├── gradle.properties
├── gradlew
├── gradlew.bat
├── hashing/
│   ├── build.gradle.kts
│   └── src/
│       └── jsMain/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── hashing/
│                               ├── HashDetails.kt
│                               ├── diff.kt
│                               └── hashFiles.kt
├── layered-cache/
│   ├── build.gradle.kts
│   └── src/
│       ├── jsMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── github/
│       │               └── burrunan/
│       │                   └── gradle/
│       │                       ├── GradleCacheAction.kt
│       │                       ├── Parameters.kt
│       │                       ├── cache/
│       │                       │   ├── ActionsTriggerExtensions.kt
│       │                       │   ├── Cache.kt
│       │                       │   ├── CompositeCache.kt
│       │                       │   ├── DefaultCache.kt
│       │                       │   ├── GradleGeneratedJarsCache.kt
│       │                       │   ├── LayeredCache.kt
│       │                       │   ├── MetadataFile.kt
│       │                       │   ├── dependenciesCache.kt
│       │                       │   └── localBuildCache.kt
│       │                       └── github/
│       │                           └── StateExtensions.kt
│       └── jsTest/
│           ├── kotlin/
│           │   └── com/
│           │       └── github/
│           │           └── burrunan/
│           │               └── gradle/
│           │                   ├── CacheServerTest.kt
│           │                   └── GlobTest.kt
│           └── resources/
│               └── readme.txt
├── renovate.json
├── settings.gradle.kts
├── test-library/
│   ├── build.gradle.kts
│   └── src/
│       └── jsMain/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── test/
│                               └── testExtensions.kt
└── wrappers/
    ├── actions-cache/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── actions/
    │                   └── cache/
    │                       ├── CacheExtensions.kt
    │                       ├── RestoreType.kt
    │                       ├── internal/
    │                       │   ├── CacheContract.kt
    │                       │   └── httpclient.kt
    │                       └── types.kt
    ├── actions-toolkit/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── actions/
    │                   ├── core/
    │                   │   ├── ActionFailedException.kt
    │                   │   ├── ActionStage.kt
    │                   │   ├── ActionsEnvironment.kt
    │                   │   ├── LogLevel.kt
    │                   │   ├── LoggingExtensions.kt
    │                   │   └── ext/
    │                   │       ├── Group.kt
    │                   │       └── InputExtensions.kt
    │                   ├── exec/
    │                   │   └── ExecExtensions.kt
    │                   └── glob/
    │                       └── removeFiles.kt
    ├── java-properties/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── javaproperties/
    │                   ├── index.module_java-properties.kt
    │                   └── parseString.kt
    ├── js/
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── com/
    │                   └── github/
    │                       └── burrunan/
    │                           ├── formatBytes.kt
    │                           └── wrappers/
    │                               └── js/
    │                                   └── SuspendExtensions.kt
    ├── nodejs/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── com/
    │                   └── github/
    │                       └── burrunan/
    │                           └── wrappers/
    │                               └── nodejs/
    │                                   ├── FsExtensions.kt
    │                                   └── StreamExtensions.kt
    ├── octokit-request-error/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── octokit/
    │                   └── requesterror/
    │                       ├── index.module_@octokit_request-error.kt
    │                       └── types.module_@octokit_request-error.kt
    ├── octokit-types/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── octokit/
    │                   └── types/
    │                       ├── AuthInterface.module_@octokit_types.kt
    │                       ├── EndpointDefaults.module_@octokit_types.kt
    │                       ├── EndpointInterface.module_@octokit_types.kt
    │                       ├── EndpointOptions.module_@octokit_types.kt
    │                       ├── Fetch.module_@octokit_types.kt
    │                       ├── GetResponseTypeFromEndpointMethod.module_@octokit_types.kt
    │                       ├── OctokitResponse.module_@octokit_types.kt
    │                       ├── RequestError.module_@octokit_types.kt
    │                       ├── RequestHeaders.module_@octokit_types.kt
    │                       ├── RequestInterface.module_@octokit_types.kt
    │                       ├── RequestOptions.module_@octokit_types.kt
    │                       ├── RequestParameters.module_@octokit_types.kt
    │                       ├── RequestRequestOptions.module_@octokit_types.kt
    │                       ├── ResponseHeaders.module_@octokit_types.kt
    │                       ├── Route.module_@octokit_types.kt
    │                       ├── Signal.module_@octokit_types.kt
    │                       ├── StrategyInterface.module_@octokit_types.kt
    │                       ├── Url.module_@octokit_types.kt
    │                       └── VERSION.module_@octokit_types.kt
    └── octokit-webhooks/
        ├── build.gradle.kts
        └── src/
            └── jsMain/
                └── kotlin/
                    └── octokit/
                        ├── ActionsTrigger.kt
                        └── webhooks/
                            ├── ResponseHeaders.module_@octokit_types.kt
                            ├── event-payloads.EventPayloads.module_@octokit_webhooks.kt
                            ├── get-webhook-payload-type-from-event.module_@octokit_webhooks.kt
                            ├── index.module_@octokit_webhooks.kt
                            └── types.module_@octokit_webhooks.kt

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
max_line_length = 140

[*.md]
trim_trailing_whitespace = false

[{*.sh,gradlew}]
end_of_line = lf

[{*.bat,*.cmd}]
end_of_line = crlf

[{*.kts,*.kt}]
indent_size = 4
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true


================================================
FILE: .gitattributes
================================================
* text=auto
*.sh text eol=lf
gradlew text eol=lf
*.bat text eol=crlf
*.jar binary


================================================
FILE: .github/workflows/check-action-typing.yml
================================================
name: Check Action Typing

on:
    push:
      paths:
        - 'action.yml'
        - 'action-types.yml'
    pull_request:
      paths:
        - 'action.yml'
        - 'action-types.yml'

jobs:
    check_action_typing:
        name: Check Action Typing
        runs-on: ubuntu-latest
        steps:
            - name: Checkout
              uses: actions/checkout@v6

            - name: Check Action Typing
              uses: krzema12/github-actions-typing@v0


================================================
FILE: .github/workflows/gradle-wrapper-validation.yml
================================================
name: "Validate Gradle Wrapper"
on: [push, pull_request]

permissions:
  contents: read

jobs:
  validation:
    name: "Validation"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: gradle/wrapper-validation-action@v3


================================================
FILE: .github/workflows/main.yml
================================================
# The default workflow for GitHub Actions that is used for continuous
# integration. A configuration file that is used to control when, where,
# and how different CI jobs are executed.
# For more information on how to modify this file check the following link:
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions

name: CI

on:
  push:
    branches:
      - '*'
  pull_request:
    branches:
      - '*'

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - name: Install Java 17
      uses: actions/setup-java@v5
      with:
        java-version: 17
        distribution: liberica
    - uses: burrunan/gradle-cache-action@v3
      name: Cache .gradle
    - name: Build
      run: ./gradlew --no-parallel --no-daemon --build-cache build
    - name: Publish release
      if: ${{ github.ref == 'refs/heads/main' }}
      env:
        PUSH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        git fetch origin refs/heads/release:release
        git checkout release
        cp cache-action-entrypoint/build/dist/js/productionExecutable/cache-action-entrypoint.js* dist/
        git checkout main action.yml
        git checkout main action-types.yml
        git add dist/ action.yml action-types.yml
        if git diff --staged --quiet; then
          echo Everything is UP-TO-DATE
        else
          echo Pushing the new release
          git fetch origin 'refs/tags/v*:refs/tags/v*'
          if [[ $(git tag --points-at HEAD) ]] || [[ "$(git log --format=%B -n 1 origin/release)" != 'Publish release from'* ]]; then
            AMEND=
            FORCE=
          else
            AMEND=--amend
            FORCE=--force
          fi
          git config --global user.email "sitnikov.vladimir@gmail.com"
          git config --global user.name "CI"
          git commit $AMEND -m "Publish release from $GITHUB_SHA"
          remote_repo="https://${GITHUB_ACTOR}:${PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
          git push $remote_repo $FORCE release
        fi


================================================
FILE: .gitignore
================================================
/.idea/
/.gradle/
/build/
/*/build/
/*/*/build/
/externals/
/*/externals/
/*/*/externals/


================================================
FILE: CHANGELOG.md
================================================
## 2024-05-03: v3 🚀 Move to actions/cache@v4 API, resolve "Cache service responded with 422"
* Bump to actions/cache@v4 API

* Bump Gradle to 8.14
* Bump to Kotlin 2.1.20
* Bump kotlinx-coroutines to 1.10.2
* Bump kotlin-serialization to 1.8.1
* Bump kotlin-wrappers to 2025.5.2

## 2024-07-25: v2, v1.21 🚀 Move to node20

* Bump `node16` to `node20`. This resolves "node16 is deprecated" warning.

* Bump Gradle to 8.8
* Bump kotlinx-coroutines to 1.8.1
* Bump kotlin-serialization to 1.7.1
* Bump wrapper-validation-action to v3
* Bump setup-java, checkout to v4

## 2023-02-18: v1.21 🚑 bump dependencies

* Add wrapper-validation-action
* Bump checkout and setup-java to v3

## 2023-02-18: v1.20 🚑 Avoid crash on missing layer-..json file

* fix: avoid failure when index restore misses layer-..json file
* Migrate to kotlin-wrappers:kotlin-actions-toolkit for better Kotlin wrappers https://github.com/burrunan/gradle-cache-action/pull/65
* Bump Gradle to 8.0.1
* Bump Kotlin to 1.8.10

## 2023-02-03: v1.19 🚑 Support nested version catalogs

* Also consider nested version catalogs in default dependency paths: https://github.com/burrunan/gradle-cache-action/issues/63

Thanks to [Vampire](https://github.com/Vampire) for the contribution.

## 2023-02-03: v1.18 🚑 fix crash when git log returns a string with a newline

* Trim the resulting SHA to prevent failures like in https://github.com/burrunan/gradle-cache-action/issues/63

## 2023-02-03: v1.17 🚀 better types for list arguments in github-workflows-kt

* Mark list arguments better: https://github.com/burrunan/gradle-cache-action/pull/61
* Add gradle/libs.versions.toml to the default dependency paths: https://github.com/burrunan/gradle-cache-action/pull/62

Thanks to [Vampire](https://github.com/Vampire) for the contribution.

## 2023-01-23: v1.16 🚀 added types for github-workflows-kt

See https://github.com/burrunan/gradle-cache-action/issues/58

Thanks to [Vampire](https://github.com/Vampire) for the contribution.

## 2022-11-27: v1.15 ⬆️ bump dependencies
Includes all the fixes from 1.13 and 1.14.

## 2022-10-29: v1.14 ⬆️ bump dependencies
*Unreleased*: the code was not compatible with `kotlin-wrappers/node`, so it did not work. Use 1.15 instead.

* bump @actions/core: 1.9.1 -> 1.10.0 (fix set-state warning)
* bump @actions/cache: 3.0.4 -> 3.0.6

## 2022-08-24: v1.13 ⬆️ bump dependencies
*Unreleased*: the code was not compatible with `kotlin-wrappers/node`, so it did not work.  Use 1.15 instead.

* bump @actions/core: 1.9.0 -> 1.9.1
* bump @actions/cache: 3.0.0 -> 3.0.4
* Move from kotlinx-node to kotlin-wrappers/node
* Print stacktrace on cache proxy server failure
* Bump Gradle to 7.5.1

## 2022-07-15: v1.12 ⬆️ bump dependencies

* Kotlin 1.4.31 legacy -> 1.7.10 IR
* @actions/cache: 1.0.1 -> 3.0.0
* @actions/core: 1.2.4 -> 1.9.0
* @actions/exec 1.0.4 -> 1.1.1
* @actions/glob 0.1.0 -> 0.3.0
* @actions/http-client 1.0.8 -> 2.0.1
* @actions/io 1.0.2 -> 1.1.2
* @actions/tool-cache 1.6.0 -> 2.0.1
* @octokit/request-error 2.0.2 -> 3.0.0
* @octokit/types 5.4.0 -> 6.39.0
* @octokit/webhooks 7.9.3 -> 10.0.8
* nodejs: 12.18.3 -> 16.16.0

Thanks to https://github.com/infomiho for https://github.com/burrunan/gradle-cache-action/pull/49

## 2022-07-06: v1.11 🚑 add home-directory property to override $HOME location

When Docker executes under root user, it will use `/root` as home directory,
so cache location would use locations like `/root/.gradle`.

The new `home-directory: /path/to/proper/user/home` property can be used to
override the location of `$HOME`.

See https://github.com/burrunan/gradle-cache-action/issues/41

## 2021-03-09: v1.10 🚀 optimize local build cache with push=false when read-only

Local build cache won't help much in the read-only mode since workers are stateless anyway.
Disabling the cache reduces the time it takes to pack cache entries.

## 2021-03-09: v1.9 🚀 optimize remote build cache with push=false when read-only

Previously `read-only` was implemented as "skip saving the caches",
however it makes sense to configure `push=false` so Gradle skips cache entry preparation as well.

## 2021-03-09: v1.8 🚑 support Gradle 5

Support Gradle 5 (and possibly even earlier versions).
Previously the plugin added `init.gradle` script to activate remote build cache aggregator.
However, `beforeSettings` is Gradle 6.0+ only, so in previous Gradle versions
`gradle-cache-action` skips `com.github.burrunan.multi-cache` plugin.

The impact is old Gradle versions would not be able to use both GitHub cache and
custom remote build cache at the same time (only project-defined remote build cache would be used).

## 2021-03-08: v1.7 🚀 support read-only cache operation

The following configuration would make all non-main branch builds to use read-only caching:
`read-only: ${{ github.ref != 'refs/heads/main' }}`.
It would save GitHub space usage for PR builds, however, they would still use
caches from the main branch.

Bump Gradle: 6.5.1 -> 6.8.3
Bump Kotlin: 1.4.0-rc -> 1.4.31
Bump kotlinx-serialization: 1.0-M1-1.4.0-rc -> 1.1.0
Bump kotlinx-serialization: 1.0-M1-1.4.0-rc -> 1.1.0
Bump kotlinx-coroutines: 1.3.8-1.4.0-rc -> 1.4.3
Bump kotlin-wrappers: 1.0.1-pre.110 -> 1.0.1-pre.148

## 2020-12-09: v1.6 🚀 added option to silence warnings

Add `gradle-distribution-sha-256-sum-warning` option to silence warning in case checksum is missing.
Add `gradle-build-scan-report` to skip publishing Gradle Build Scan URL to the job report.

Thanks to [Przemysław Jakubczyk](https://github.com/pjakubczyk) for the contributions.

## 2020-09-24: v1.5 🚑 support branch names with slashes

`feature/branch` is a valid branch name in Git, so `gradle-cache-action` now escapes `/` when
using branch name as a part of the cache key.

## 2020-08-20: v1.4 🚑 better exclusions for *.lock files

It turns out caching action can't apply exclude if user included of the parent folders.

See https://github.com/actions/cache/issues/364#issuecomment-678118231

It should fix errors like

```
C:\windows\System32\tar.exe -z -cf cache.tgz -P -C D:/a/... --files-from manifest.txt
tar.exe: Couldn't open C:/Users/runneradmin/.gradle/caches/6.6/generated-gradle-jars/generated-gradle-jars.lock: Permission denied
tar.exe: Error exit delayed from previous errors.
```

## 2020-08-20 🙈 silence insecure protocols warning

Modern Gradle versions issue a warning when users configure `http://` build cache.
In practice, GitHub-based cache is located on localhost, so it is fine to use http.

The plugin adds the relevant configuration to silence Gradle warning.

## 2020-08-19 🚀 Unlock Gradle remote build caching

* Add HTTP caching proxy that implements Gradle HTTP cache API [effb04a](https://github.com/burrunan/gradle-cache-action/commit/effb04a)

This enables Gradle to use GitHub cache API like a regular remote build cache service,
so the caching is more efficient. Gradle fetches only the objects it needs,
and it uploads only what was changed. Cache eviction is managed by GitHub.

The remote build cache feature activates when you use `with: arguments: build ...`
to launch the build. In other words, you need to launch Gradle via `gradle-cache-action`
rather that regular `run:` or `gradle-command-action`.

Here's how you can integrate build cache to existing projects:

* Apache Calcite: https://github.com/apache/calcite/pull/2114
* Apache JMeter: https://github.com/apache/jmeter/pull/611
* pgjdbc: https://github.com/pgjdbc/pgjdbc/pull/1862
* junit-pioneer: https://github.com/junit-pioneer/junit-pioneer/pull/325
* opentelemetry-java-instrumentation: https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/1054

### Fixes

* Reduce verbosity of "cache already exists" warning to info [8ff7dd7](https://github.com/burrunan/gradle-cache-action/commit/8ff7dd7)
* Gradle dependencies: treat *.gradle.kts as a part of the cache key [1c59269](https://github.com/burrunan/gradle-cache-action/commit/1c59269)
* Parse properties from properties tag rather than from arguments [140d8d9](https://github.com/burrunan/gradle-cache-action/commit/140d8d9)
* Add wrappers [597d5ea](https://github.com/burrunan/gradle-cache-action/commit/597d5ea)

## 2020-08-15 Support schedule and workflow_dispatch events

* Treat workflow_dispatch event (manual launch) the same as "build from the default branch) [48e88ce](https://github.com/burrunan/gradle-cache-action/commit/48e88ce)
* Use defaultbranch for schedule-based builds by default [8d3e9cc](https://github.com/burrunan/gradle-cache-action/commit/8d3e9cc)
* 🐛 Avoid build failures on schedule events: use a fixed "defaultbranch" instead of computing the branch name [6957760](https://github.com/burrunan/gradle-cache-action/commit/6957760)

### Fixes

* 🥅 Ignore unreadable files when hasing (and print warning) [01d4c8f](https://github.com/burrunan/gradle-cache-action/commit/01d4c8f)
* 🐛 avoid adding /**/ mask to Gradle dependencies key [6d3e893](https://github.com/burrunan/gradle-cache-action/commit/6d3e893)
* Add exception message to hashFiles(...) [d5f863c](https://github.com/burrunan/gradle-cache-action/commit/d5f863c)
* Disable minification for better error reporting in GitHub [29d2590](https://github.com/burrunan/gradle-cache-action/commit/29d2590)
* 🐛 Add missing await in mkdir(String) [e4a441d](https://github.com/burrunan/gradle-cache-action/commit/e4a441d)
* 🐛 Add missing JsModule declaration [54f0245](https://github.com/burrunan/gradle-cache-action/commit/54f0245)
* 🐛 Fix release publishing [b52aa2f](https://github.com/burrunan/gradle-cache-action/commit/b52aa2f)
* Split modules [365cc69](https://github.com/burrunan/gradle-cache-action/commit/365cc69)
* 🔨 Split modules [ec0c31a](https://github.com/burrunan/gradle-cache-action/commit/ec0c31a)
* ✅ Add basic tests for cache store and restore, fix invalid "always partial restore" status [594213e](https://github.com/burrunan/gradle-cache-action/commit/594213e)
* Add test for hashFilesDetailed [ce7fa0f](https://github.com/burrunan/gradle-cache-action/commit/ce7fa0f)

## 2020-07-27

✨ add gradle-dependencies-cache-key property for configuring extra dependencies (e.g. versions declared in buildSrc/Versions.kt) [cc7a294](https://github.com/burrunan/gradle-cache-action/commit/cc7a294)

## 2020-07-27 🎉 Initial version

The following caches are saved and restored:
* Gradle dependencies (~/.gradle/caches/modules-2)
* Gradle local build cache (~/.gradle/caches/build-cache-1)
* Gradle generated jars (~/.gradle/caches/*.*/generated-gradle-jars)
* Maven local repository (~/.m2/repository)


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS


================================================
FILE: NOTICE
================================================
Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>


================================================
FILE: README.md
================================================
# Gradle Cache Action

[![CI Status](https://github.com/burrunan/gradle-cache-action/workflows/CI/badge.svg)](https://github.com/burrunan/gradle-cache-action/actions)
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/burrunan/gradle-cache-action?label=release)

This is a GitHub Action for caching Gradle caches.
In other words, this is [@actions/cache](https://github.com/actions/cache) customized for Gradle.

Key improvements over [@actions/cache](https://github.com/actions/cache) and [gradle-command-action](https://github.com/eskatos/gradle-command-action) are:
- 🚀 Gradle remote build cache backend (pulls only the needed entries from GitHub cache)
- 🎉 Support multiple remote caches via [gradle-multi-cache](https://github.com/burrunan/gradle-multi-cache) (e.g. GitHub Actions + S3)
- 👋 Simplified configuration (action name + gradle command is enough for most case)
- 👾 Less space usage (GitHub imposes overall 5GiB limit by default, so cache space matters)
- 🔗 Link to Build Scan in build results
- 💡 Gradle build failure markers added to the diff view (e.g. `compileJava` or `compileKotlin` markers right in the commit diff)

## Version notes

`v1` uses `node16` which has been deprecated, so consider upgrading to `v2`.
The upgrade requires only changing the version, however the newer `node20` might be missing
if you use an old runner.

## Usage

Add the following code to your workflow file in the `.github/workflows` directory.

Note: Like with [gradle-command-action](https://github.com/eskatos/gradle-command-action), you can
specify `gradle-version: release` to test with the current release version of Gradle, `gradle-version: nightly` for testing Gradle nightly builds,
an so on (see `gradle-version` below).

Note: For the [security reasons](https://julienrenaux.fr/2019/12/20/github-actions-security-risk/)
you might want to use Git SHA rather than branch name or tag name.
In other words, to avoid accidental tag update, you might want to use
`burrunan/gradle-cache-action@03c71a8ba93d670980695505f48f49daf43704a6` rather than `burrunan/gradle-cache-action@v1`.
Please see releases page to find out the commit ids: https://github.com/burrunan/gradle-cache-action/releases

You might use the following references are:
* `v1`, `v2` -- this is a *moving* qualifier. It points to the latest release among `v1.x`
* `v1.0`, `v1.1`, ... -- those are fixed versions. They won't change over time

For the best security you might want to use `burrunan/gradle-cache-action@v3` (see the ids at https://github.com/burrunan/gradle-cache-action/releases)

```yaml
- uses: burrunan/gradle-cache-action@v3
  name: Build PROJECT_NAME
  # Extra environment variables for Gradle execution (regular GitHub Actions feature)
  # Note: env must be outside of "with"
  env:
    VARIABLE: VALUE
  with:
    # If you have multiple jobs, use distinct job-id in in case you want to split caches
    # For instance, jobs with different JDK versions can't share caches
    # RUNNER_OS is added to job-id automatically
    job-id: jdk8
    # Specifies arguments for Gradle execution
    # If arguments is missing or empty, then Gradle is not executed
    arguments: build
    # arguments can be multi-line for better readability
    # arguments: |
    #  --no-paralell
    #  build
    #  -x test
    # Gradle version to use for execution:
    #   wrapper (default), current, rc, nightly, release-nightly, or
    #   versions like 6.6 (see https://services.gradle.org/versions/all)
    gradle-version: wrapper
    # Properties are passed as -Pname=value
    properties: |
      kotlin.js.compiler=ir
      kotlin.parallel.tasks.in.project=true
```

By default, the action enables the `local` build cache, and it adds a remote build cache
that stores the data in GitHub Actions cache.
However, you might want to enable the [Gradle Build Cache](https://docs.gradle.org/current/userguide/build_cache.html)
for your local builds to make them faster, or even add a remote cache instance, so your local
builds can reuse artifacts that are build on CI.

This is how you can enable local build cache (don't forget to add `--build-cache` option or
`org.gradle.caching=true` property):

```kotlin
// settings.gradle.kts
val isCiServer = System.getenv().containsKey("CI")
// Cache build artifacts, so expensive operations do not need to be re-computed
buildCache {
   local {
       isEnabled = !isCiServer
   }
}
```

## Sample integrations

Here's how you can integrate build cache to existing projects:

* Apache Calcite: https://github.com/apache/calcite/pull/2114
* Apache JMeter: https://github.com/apache/jmeter/pull/611
* pgjdbc: https://github.com/pgjdbc/pgjdbc/pull/1862
* junit-pioneer: https://github.com/junit-pioneer/junit-pioneer/pull/325
* opentelemetry-java-instrumentation: https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/1054

## Configuration

The default configuration should suit most of the cases, however, there are extra knobs:

```yaml
- uses: burrunan/gradle-cache-action@v3
  name: Cache .gradle
  # Extra environment variables for Gradle execution (regular GitHub Actions feature)
  env:
    VARIABLE: VALUE
  with:
    # If you have multiple jobs, use distinct job-id in case you want to split caches
    # For instance, jobs with different JDK versions can't share caches
    # RUNNER_OS is added to job-id automatically
    job-id: jdk8

    # Overrides $HOME
    # home-directory: /home/user

    # Disable caching of $HOME/.gradle/caches/*.*/generated-gradle-jars
    save-generated-gradle-jars: false

    # Disable remote cache that proxies requests to GitHub Actions cache
    remote-build-cache-proxy-enabled: false

    # Set the cache key for Gradle version (e.g. in case multiple jobs use different versions)
    # By default the value is `wrapper`, so the version is determined from the gradle-wrapper.properties   
    # Note: this argument specifies the version for Gradle execution (if `arguments` is present)
    # Supported values:
    #   wrapper (default), current, rc, nightly, release-nightly, or
    #   versions like 6.6 (see https://services.gradle.org/versions/all)
    gradle-version: 6.5.1-custom

    # Makes all non-main branch builds to use read-only caching
    read-only: ${{ github.ref != 'refs/heads/main' }}

    # Uncomment to keep Gradle Daemon after the build
    # daemon: true

    # Arguments for Gradle execution
    arguments: build jacocoReport

    # Properties are passed as -Pname=value
    properties: |
      kotlin.js.compiler=ir
      kotlin.parallel.tasks.in.project=true

    # Relative path under $GITHUB_WORKSPACE where Git repository is placed
    build-root-directory: sub/directory

    # Activates only the caches that are relevant for executing gradle command.
    # This is helpful when build job executes multiple gradle commands sequentially.
    # Then the caching is implemented in the very first one, and the subsequent should be marked
    # with execution-only-caches: true
    execution-only-caches: true

    # Disable caching of ~/.gradle/caches/build-cache-*
    save-local-build-cache: false

    # Disable caching of ~/.gradle/caches/modules-*
    save-gradle-dependencies-cache: false

    # Extra files to take into account for ~/.gradle/caches dependencies
    gradle-dependencies-cache-key: |
      gradle/dependencies.kt
      buildSrc/**/Version.kt

    # Disable caching of ~/.m2/repository/
    save-maven-dependencies-cache: false

    # Ignore some of the paths when caching Maven Local repository
    maven-local-ignore-paths: |
      org/example/
      com/example/

    # Enable concurrent cache save and restore
    # Default is concurrent=false for better log readability
    concurrent: true

    # Disable publishing Gradle Build Scan URL to job report
    gradle-build-scan-report: false

    # Disable warning about missing distributionSha256Sum property in gradle-wrapper.properties
    gradle-distribution-sha-256-sum-warning: false
```

## How does dependency caching work?

The current GitHub Action's cache (both [actions/cache](https://github.com/actions/cache) action and
[@actions/cache](https://github.com/actions/toolkit/tree/main/packages/cache) npm package) is immutable.

The cache can't be updated, so it does not work very good for caches like "Gradle dependencies" or "Maven local repository".

`gradle-cache-action` creates a layered cache, and it uses a small "index" cache to identify the required layers.
If only a small fraction of files changes, then the action reuses the existing caches, and it adds a layer on top of it.
That enables to save cache space (GitHub has a default limit of 5 GiB), and it reduces upload time as only
the cache receives only the updated files.

## How does GitHub Actions-based Gradle remote build cache work?

`gradle-cache-action` launches a small proxy server that listens for Gradle requests and
then it redirects the requests to the `@actions/cache` API.

That makes Gradle believe it is talking to a regular remote cache, and the cache receives
only the relevant updates.
The increased granularity enables GitHub to evict entries better (it removes unused entries
automatically).

The action configures the URL to the cache proxy via the `~/.gradle/init.gradle` script, and
[Gradle picks it up automatically](https://docs.gradle.org/current/userguide/init_scripts.html)

Note: Saving GitHub Actions caches might take noticeable time (e.g. 100 ms), so the cache uploads
in the background. In other words, build scan would show virtually zero response times for
cache save operations.

If your build already has a remote cache declared (e.g. you are using your own cache),
then `gradle-cache-action` would configure **both** remote caches.
It would read from the GitHub cache first, and it would save data to both caches.

The multi-cache feature can be disabled via `multi-cache-enabled: false`.

## How to enable build scans?

1. Read and agree to the terms of service: https://gradle.com/terms-of-service
1. Add `--scan` to `arguments:`, and add the following to `settings.gradle.kts`

```kotlin
plugins {
    `gradle-enterprise`
}

val isCiServer = System.getenv().containsKey("CI")

if (isCiServer) {
    gradleEnterprise {
        buildScan {
            termsOfServiceUrl = "https://gradle.com/terms-of-service"
            termsOfServiceAgree = "yes"
            tag("CI")
        }
    }
}
```

## Why another action instead of gradle-command-action?

`gradle-command-action` was started as a Kotlin/JS experiment for making a customized
[@actions/cache](https://github.com/actions/cache) that would make Gradle builds faster.

Then it turned out there's a proxy remote cache requests to the `@actions/cache` API can be used when the caching
action executes Gradle, so the `gradle-cache-action` got a Gradle execution feature.

Of course, the same could have been made in [gradle-command-action](https://github.com/eskatos/gradle-command-action),
however:
- The author was not familiar with TypeScript ecosystem (stdlib, typical libraries, testing libraries, etc.)
- Caching logic is collections-heavy, and Kotlin stdlib shines here.

  For instance, in Kotlin `list + list` adds lists, and `array.associateWith { valueFor(it) }` converts arrays to maps.
  This is easy to write without consulting StackOverflow, the code is readable, and it does not require
  you [to fight with the compiler](https://blog.johnnyreilly.com/2016/06/create-es2015-map-from-array-in-typescript.html).

- A single language helps when building connected components.
  `gradle-cache-action` integrates with [gradle-multi-cache](https://github.com/burrunan/gradle-multi-cache) and
  [gradle-s3-build-cache](https://github.com/burrunan/gradle-s3-build-cache), and they all are Kotlin-based. 

## Can I use the caching part of the action only?

Yes, you can. If you omit `arguments:`, then the action runs in `cache-only` mode.
It won't launch Gradle.

## Can I call multiple different Gradle builds in the same job?

This might be complicated, see https://github.com/burrunan/gradle-cache-action/issues/15.

Currently, the workaround is to configure `execution-only-caches: true` for all but one
`gradle-cache-action` executions.
Then one of the actions would do the cache save and restore, and the rest would use their own
caches only.

## Contributing

Contributions are always welcome! If you'd like to contribute (and we hope you do) please open a pull request.

## License

Apache 2.0

## Author

Vladimir Sitnikov <sitnikov.vladimir@gmail.com>


================================================
FILE: action-types.yml
================================================
# See https://github.com/krzema12/github-actions-typing
outputs:
  build-scan-url:
    type: string
inputs:
  job-id:
    type: string
  build-root-directory:
    type: string
  home-directory:
    type: string
  gradle-version:
    type: string
  read-only:
    type: boolean
  save-generated-gradle-jars:
    type: boolean
  save-local-build-cache:
    type: boolean
  multi-cache-enabled:
    type: boolean
  multi-cache-version:
    type: string
  multi-cache-repository:
    type: string
  multi-cache-group-id-filter:
    type: string
  save-gradle-dependencies-cache:
    type: boolean
  execution-only-caches:
    type: boolean
  remote-build-cache-proxy-enabled:
    type: boolean
  gradle-dependencies-cache-key:
    type: list
    separator: "\n"
    list-item:
      type: string
  save-maven-dependencies-cache:
    type: boolean
  maven-local-ignore-paths:
    type: list
    separator: "\n"
    list-item:
      type: string
  debug:
    type: boolean
  concurrent:
    type: boolean
  arguments:
    type: list
    separator: "\n"
    list-item:
      type: string
  properties:
    type: list
    separator: "\n"
    list-item:
      type: string
  gradle-build-scan-report:
    type: boolean
  gradle-distribution-sha-256-sum-warning:
    type: boolean


================================================
FILE: action.yml
================================================
name: 'Gradle Cache'
description: 'Caches .gradle folder (dependencies, local build cache, ...)'
author: 'Vladimir Sitnikov'
outputs:
  build-scan-url:
    description: Link to the build scan if any
inputs:
  job-id:
    description: A job identifier to avoid cache pollution from different jobs
    required: false
  build-root-directory:
    description: Relative path under $GITHUB_WORKSPACE where Git repository is placed
    required: false
  home-directory:
    description: Overrides the location of $HOME (e.g. to avoid use of /root when running in Docker)
    required: false
  gradle-version:
    description: (wrapper | or explicit version) Caches often depend on the Gradle version, so this parameter sets the ID to use for cache keys. It does not affect the Gradle version used for build
    required: false
    default: wrapper
  read-only:
    description: Configures caches for read-only opreration (e.g. to save GitHub Actions storage limit)
    required: false
    default: false
  save-generated-gradle-jars:
    description: Enables caching of $HOME/.gradle/caches/*.*/generated-gradle-jars
    required: false
    default: 'true'
  save-local-build-cache:
    description: Enables caching of $HOME/.gradle/caches/build-cache-1
    required: false
    default: 'true'
  multi-cache-enabled:
    description: Adds com.github.burrunan.multi-cache plugin to settings.gradle so GitHub Actions cache can be used in parallel with Gradle remote build cache
    required: false
    default: 'true'
  multi-cache-version:
    description: Configures com.github.burrunan.multi-cache version to use
    required: false
    default: '1.0'
  multi-cache-repository:
    description: Configures repository where com.github.burrunan.multi-cache can be located
    required: false
    default: ''
  multi-cache-group-id-filter:
    description: Configures group id for selecting only com.github.burrunan.multi-cache artifacts (it enables Gradle to use custom repository for multi-cache only)
    required: false
    default: 'com[.]github[.]burrunan[.]multi-?cache'
  save-gradle-dependencies-cache:
    description: Enables caching of ~/.gradle/caches/modules-*
    required: false
    default: 'true'
  execution-only-caches:
    description: |
      Activates only the caches that are relevant for executing gradle command.
      This is helpful when build job executes multiple gradle commands sequentially.
      Then the caching is implemented in the very first one, and the subsequent should be marked
      with execution-only-caches: true
    required: false
    default: 'false'
  remote-build-cache-proxy-enabled:
    description: Activates a remote cache that proxies requests to GitHub Actions cache
    required: false
    default: 'true'
  gradle-dependencies-cache-key:
    description: Extra files to take into account for ~/.gradle/caches dependencies
    required: false
  save-maven-dependencies-cache:
    description: Enables caching of ~/.m2/repository/
    required: false
    default: 'true'
  maven-local-ignore-paths:
    description: Specifies ignored paths in the Maven Local repository (e.g. the artifacts of the current project)
    required: false
    default: ''
  debug:
    description: Shows extra logging to debug the action
    required: false
    default: 'true'
  daemon:
    description: Enables or disables Gradle Daemon (--no-daemon). Defaults to false as typically the build executes only once in CI, so there's no much value in keeping the daemon
    required: false
    default: 'false'
  concurrent:
    description: Enables concurent cache download and upload (disabled by default for better log output)
    required: false
    default: 'false'
  arguments:
    description: Gradle arguments to pass (optionally multiline)
    required: false
  properties:
    description: Extra Gradle properties (multiline) which would be passed as -Pname=value arguments
    required: false
  gradle-build-scan-report:
    description: Publishes Gradle Build Scan URL to job report.
    required: false
    default: 'true'
  gradle-distribution-sha-256-sum-warning:
    description: Enables warning when distributionSha256Sum property is missing in gradle-wrapper.properties
    required: false
    default: 'true'
runs:
  using: node20
  main: dist/cache-action-entrypoint.js
  post: dist/cache-action-entrypoint.js
branding:
  icon: archive
  color: gray-dark


================================================
FILE: build.gradle.kts
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

plugins {
    kotlin("multiplatform") apply false
}

plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
    configure<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec> {
        version = "22.0.0"
    }
}

subprojects {
    if (path != ":wrappers") {
        apply(plugin = "org.jetbrains.kotlin.multiplatform")
    }
}

allprojects {
    plugins.withId("org.jetbrains.kotlin.multiplatform") {
        configure<KotlinMultiplatformExtension> {
            js {
                compilerOptions {
                    target = "es2015"
                }
            }
        }
        tasks {
            withType<AbstractTestTask>().configureEach {
                testLogging {
                    showStandardStreams = true
                }
            }
        }
        configure<KotlinMultiplatformExtension> {
            js {
                if (project.name.endsWith("-entrypoint")) {
                    browser {
                        testTask {
                            useMocha {
                                timeout = "10000"
                            }
                        }
                    }
                    binaries.executable()
                } else {
                    nodejs {
                        testTask {
                            useMocha {
                                timeout = "10000"
                                environment("RUNNER_TOOL_CACHE", layout.buildDirectory.dir("tool-cache").get().asFile.toString())
                            }
                        }
                    }
                }
            }
        }
        dependencies {
            "commonMainApi"(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
            "commonMainApi"(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
            "jsMainImplementation"(enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:2025.12.7"))
            if (project.path != ":test-library") {
                "jsTestImplementation"(rootProject.projects.testLibrary)
            }
        }
    }
}


================================================
FILE: cache-action-entrypoint/build.gradle.kts
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

kotlin {
    sourceSets {
        jsMain {
            dependencies {
                implementation(projects.cacheProxy)
                implementation(projects.gradleLauncher)
                implementation(projects.layeredCache)
                implementation(projects.wrappers.actionsToolkit)
                implementation(projects.wrappers.nodejs)
                implementation(projects.wrappers.octokitWebhooks)
                implementation("org.jetbrains.kotlin-wrappers:kotlin-actions-io")
                implementation(npm("string-argv", "0.3.1"))
            }
        }
    }
}


================================================
FILE: cache-action-entrypoint/src/jsMain/kotlin/main.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import actions.core.*
import actions.core.ext.getInput
import actions.core.ext.getListInput
import actions.io.mkdirP
import com.github.burrunan.gradle.GradleCacheAction
import com.github.burrunan.gradle.Parameters
import com.github.burrunan.gradle.github.stateVariable
import com.github.burrunan.gradle.proxy.CacheProxy
import com.github.burrunan.launcher.LaunchParams
import com.github.burrunan.launcher.install
import com.github.burrunan.launcher.launchGradle
import com.github.burrunan.launcher.resolveDistribution
import com.github.burrunan.wrappers.nodejs.normalizedPath
import js.globals.globalThis
import node.buffer.BufferEncoding
import node.fs.writeFile
import node.path.path
import node.process.process
import octokit.currentTrigger

fun String.splitLines() =
    split(Regex("\\s*[\r\n]+\\s*"))
        .filter { !it.startsWith("#") && it.contains("=") }
        .associate {
            val values = it.split(Regex("\\s*=\\s*"), limit = 2)
            values[0] to (values.getOrNull(1) ?: "")
        }

fun isMochaRunning() =
    arrayOf("afterEach", "after", "beforeEach", "before", "describe", "it").all {
        globalThis[it] is Function<*>
    }

suspend fun main() {
    if (isMochaRunning()) {
        // Ignore if called from tests
        return
    }
    val stageVar = stateVariable("stage") { "MAIN" }
    val stage = ActionStage.values().firstOrNull { it.name == stageVar.get() }
    // Set next stage
    stageVar.set(
        when (stage) {
            ActionStage.MAIN -> ActionStage.POST
            null -> {
                setFailed("Unable to find action stage: ${stageVar.get()}")
                return
            }
            else -> null
        }?.name ?: "FINAL",
    )
    try {
        mainInternal(stage)
    } catch (e: ActionFailedException) {
        setFailed(e.message)
    }
}

suspend fun mainInternal(stage: ActionStage) {
    val homeDirectory = getInput("home-directory").trimEnd('/', '\\')
    if (homeDirectory != "") {
        info("Overriding home directory to $homeDirectory")
        process.env["HOME"] = homeDirectory
    }

    val gradleStartArguments = parseArgsStringToArgv(getInput("arguments")).toList()
    val cacheProxyEnabled = getInput("remote-build-cache-proxy-enabled").ifBlank { "true" }.toBoolean()

    val executionOnlyCaches = getInput("execution-only-caches").ifBlank { "false" }.toBoolean()
    val enableBuildScanReport = getInput("gradle-build-scan-report").ifBlank { "true" }.toBoolean()

    val buildRootDirectory = getInput("build-root-directory").trimEnd('/', '\\')
    if (buildRootDirectory != "") {
        info("changing working directory to $buildRootDirectory")
        process.chdir(buildRootDirectory)
    }

    val params = Parameters(
        jobId = ActionsEnvironment.RUNNER_OS + "-" + getInput("job-id"),
        path = ".",
        debug = getInput("debug").toBoolean(),
        generatedGradleJars = getInput("save-generated-gradle-jars").ifBlank { "true" }.toBoolean(),
        localBuildCache = (!cacheProxyEnabled || gradleStartArguments.isEmpty()) && getInput("save-local-build-cache").ifBlank { "true" }
            .toBoolean(),
        gradleDependenciesCache = !executionOnlyCaches && getInput("save-gradle-dependencies-cache").ifBlank { "true" }.toBoolean(),
        gradleDependenciesCacheKey = getListInput("gradle-dependencies-cache-key"),
        mavenDependenciesCache = !executionOnlyCaches && getInput("save-maven-dependencies-cache").ifBlank { "true" }.toBoolean(),
        mavenLocalIgnorePaths = getListInput("maven-local-ignore-paths"),
        concurrent = getInput("concurrent").ifBlank { "false" }.toBoolean(),
        readOnly = getInput("read-only").ifBlank { "false" }.toBoolean(),
    )

    val gradleDistribution = resolveDistribution(
        versionSpec = getInput("gradle-version").ifBlank { "wrapper" },
        projectPath = params.path,
        distributionUrl = getInput("gradle-distribution-url").ifBlank { null },
        distributionSha256Sum = getInput("gradle-distribution-sha-256-sum").ifBlank { null },
        enableDistributionSha256SumWarning = getInput("gradle-distribution-sha-256-sum-warning").ifBlank { "true" }.toBoolean(),
    )

    if (stage == ActionStage.MAIN || stage == ActionStage.POST) {
        val cacheAction = GradleCacheAction(currentTrigger(), params, gradleDistribution)

        if (params.generatedGradleJars || params.localBuildCache ||
            params.gradleDependenciesCache || params.mavenDependenciesCache
        ) {
            cacheAction.execute(stage)
        }
    }

    if (stage == ActionStage.MAIN && gradleStartArguments.isNotEmpty()) {
        val args = when (params.localBuildCache || cacheProxyEnabled) {
            true -> listOf("--build-cache") + gradleStartArguments
            else -> gradleStartArguments
        }
        val launchParams = LaunchParams(
            gradle = install(gradleDistribution),
            daemon = getInput("daemon").ifBlank { "false" }.toBoolean(),
            projectPath = params.path,
            arguments = args,
            properties = getInput("properties").splitLines(),
        )

        val cacheProxy = CacheProxy()

        if (cacheProxyEnabled) {
            info("Starting remote cache proxy, adding it via ~/.gradle/init.gradle")
            cacheProxy.start()
            val gradleHome = path.join("~".normalizedPath, ".gradle")
            mkdirP(gradleHome)
            writeFile(
                path.join(gradleHome, "init.gradle"),
                cacheProxy.getMultiCacheConfiguration(
                    multiCacheEnabled = getInput("multi-cache-enabled").ifBlank { "true" }.toBoolean(),
                    multiCacheVersion = getInput("multi-cache-version").ifBlank { "1.0" },
                    multiCacheRepository = getInput("multi-cache-repository"),
                    multiCacheGroupIdFilter = getInput("multi-cache-group-id-filter").ifBlank { "com[.]github[.]burrunan[.]multi-?cache" },
                    push = !params.readOnly,
                ),
                BufferEncoding.utf8,
            )
        }

        try {
            val result = launchGradle(launchParams)
            if (enableBuildScanReport) {
                result.buildScanUrl?.let {
                    warning("Gradle Build Scan: $it")
                    setOutput("build-scan-url", it)
                }
            }
        } finally {
            if (cacheProxyEnabled) {
                cacheProxy.stop()
            }
        }
    }
    return
}


================================================
FILE: cache-action-entrypoint/src/jsMain/kotlin/stringArgv.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:JsModule("string-argv")

external fun parseArgsStringToArgv(value: String, env: String = definedExternally, file: String = definedExternally): Array<String>


================================================
FILE: cache-action-entrypoint/src/jsTest/kotlin/com/github/burrunan/ArgumentsTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan

import parseArgsStringToArgv
import kotlin.test.Test
import kotlin.test.assertEquals

class ArgumentsTest {
    private fun parse(input: String, vararg output: String) {
        assertEquals(listOf(*output), parseArgsStringToArgv(input).toList(), input)
    }

    @Test
    fun simple() {
        parse("")
        parse("a b", "a", "b")
        parse("a 'b'", "a", "b")
        parse("a \"b\"", "a", "b")
    }

    @Test
    fun multiline() {
        parse("a\nb", "a", "b")
        parse("a\n  b", "a", "b")
        parse("a\n  b  ", "a", "b")
        parse("a\n  b  \nc", "a", "b", "c")
    }

    @Test
    fun multilineWithQuotes() {
        parse("'a\nb'", "a\nb")
        parse("hello 'a\n  b' world", "hello", "a\n  b", "world")
        parse("hello \"a\n  b\" world", "hello", "a\n  b", "world")
    }

    @Test
    fun withDollars() {
        parse("\$HOME", "\$HOME")
    }

    @Test
    fun multilineWithComments() {
        // TODO: "# commented" should be ignored
        parse("""
            build
            # commented
            test
        """.trimIndent(), "build", "#", "commented", "test")
    }
}


================================================
FILE: cache-action-entrypoint/src/jsTest/kotlin/com/github/burrunan/SplitLinesTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan

import splitLines
import kotlin.test.Test
import kotlin.test.assertEquals

class SplitLinesTest {
    @Test
    fun empty() {
        assertEquals(mapOf("a" to "b", "c" to ""), "a=b\nc=".splitLines())
    }

    @Test
    fun withoutEquals() {
        assertEquals(mapOf("a" to "b"), "a=b\nc".splitLines())
    }
}


================================================
FILE: cache-action-entrypoint/webpack.config.d/config.js
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

config.output = config.output || {}
config.output.globalObject = "this"
config.target = "node"

config.resolve.modules.unshift("src/test/resources")

const TerserPlugin = require('terser-webpack-plugin');

// keep_classnames is required to workaround node-fetch Expected signal to be an instanceof AbortSignal
config.optimization = {
  minimizer: [
    new TerserPlugin({
                       parallel: true,
                       terserOptions: {
                         // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
                         mangle: false,
                         sourceMap: true,
                         // compress: false,
                         keep_classnames: /AbortSignal/,
                         keep_fnames: /AbortSignal/,
                         output: {
                           beautify: true,
                           indent_level: 1
                         }
                       }
                     }),
  ],
};


================================================
FILE: cache-proxy/build.gradle.kts
================================================
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.targets.js.testing.mocha.KotlinMocha

/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

kotlin {
    sourceSets {
        jsMain {
            dependencies {
                implementation(projects.cacheServiceMock)
                implementation(projects.wrappers.actionsCache)
                implementation(projects.wrappers.actionsToolkit)
                implementation(projects.wrappers.nodejs)
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-core")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
            }
        }
    }
}

tasks.withType<KotlinJsTest>().configureEach {
    (testFramework as KotlinMocha).timeout = "60000"
}


================================================
FILE: cache-proxy/src/jsMain/kotlin/com/github/burrunan/gradle/proxy/CacheProxy.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.gradle.proxy

import actions.cache.RestoreType
import actions.cache.restoreAndLog
import actions.core.LogLevel
import actions.glob.removeFiles
import com.github.burrunan.gradle.cache.HttpException
import com.github.burrunan.gradle.cache.handle
import com.github.burrunan.wrappers.nodejs.mkdir
import com.github.burrunan.wrappers.nodejs.pipeAndWait
import js.objects.unsafeJso
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import node.fs.createReadStream
import node.fs.createWriteStream
import node.fs.stat
import node.http.IncomingMessage
import node.http.OutgoingHttpHeaders
import node.http.ServerResponse
import node.net.AddressInfo
import node.path.path
import node.process.process
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class CacheProxy {
    companion object {
        const val GHA_CACHE_URL = "GHA_CACHE_URL"
        private const val TEMP_DIR = ".cache-proxy"
        private val cacheVersion = "1-"
    }

    private var _cacheUrl: String? = null

    val cacheUrl: String? get() = _cacheUrl

    private val server = node.http.createServer<IncomingMessage, ServerResponse<*>> { req, res ->
        val query = node.url.parse(req.url!!, true)
        val path = query.pathname ?: ""
        res.handle {
            val id = path.removePrefix("/")
            when (req.method) {
                "GET" -> getEntry(id, res)
                "PUT" -> putEntry(id, req, res)
                else -> HttpException.notImplemented("Not implemented: ${req.method}")
            }
        }
    }

//    private val compression = jso<InternalCacheOptions> { compressionMethod = CompressionMethod.Gzip }

    private suspend fun putEntry(id: String, req: IncomingMessage, res: ServerResponse<*>) {
        val fileName = path.join(TEMP_DIR, "bc-$id")
        try {
            req.pipeAndWait(createWriteStream(fileName))
            res.writeHead(200, "OK", undefined.unsafeCast<OutgoingHttpHeaders>())
        } finally {
            GlobalScope.launch {
                try {
                    actions.cache.saveAndLog(listOf(fileName), id, cacheVersion, logLevel = LogLevel.DEBUG)
                } finally {
                    removeFiles(listOf(fileName))
                }
            }
        }
    }

    private suspend fun getEntry(id: String, res: ServerResponse<*>) {
        val fileName = path.join(TEMP_DIR, "bc-$id")
        val restoreType = restoreAndLog(listOf(fileName), id, restoreKeys = listOf(), version = cacheVersion, logLevel = LogLevel.DEBUG)
        if (restoreType == RestoreType.None) {
            throw HttpException.notFound("No cache entry found for $id")
        }
        res.writeHead(
            200, "Ok",
            unsafeJso<OutgoingHttpHeaders> {
                contentLength = stat(fileName).size
            },
        )
        createReadStream(fileName).pipeAndWait(res)
    }

    private val pluginId = "com.github.burrunan.multi-cache"

    fun getMultiCacheConfiguration(
        multiCacheEnabled: Boolean = true,
        multiCacheVersion: String = "1.0",
        multiCacheRepository: String = "",
        multiCacheGroupIdFilter: String = "com[.]github[.]burrunan[.]multi-?cache",
        push: Boolean = true,
    ): String {
        val multiCacheGroupIdFilterEscaped = multiCacheGroupIdFilter.replace("\\", "\\\\")
        //language=Groovy
        return """
            def pluginId = 'com.github.burrunan.multi-cache'
            def multiCacheVersion = '1.0'
            def multiCacheGroupIdFilter = 'com[.]github[.]burrunan[.]multi-?cache'
            boolean multiCacheEnabled = $multiCacheEnabled
            String multiCacheRepository = '$multiCacheRepository'
            boolean gradle6Plus = org.gradle.util.GradleVersion.current() >= org.gradle.util.GradleVersion.version('6.0')
            // beforeSettings is Gradle 6.0+
            if (multiCacheEnabled && !gradle6Plus) {
                println("Multiple remote build caches ($pluginId) are supported in Gradle 6.0+ only")
                multiCacheEnabled = false
            }
            if (multiCacheEnabled) {
                beforeSettings { settings ->
                    def repos = settings.buildscript.repositories
                    if (multiCacheRepository != '') {
                        repos.add(
                            repos.maven {
                                url = multiCacheRepository
                                if ('$multiCacheGroupIdFilterEscaped' != '') {
                                    content {
                                        includeGroupByRegex('$multiCacheGroupIdFilterEscaped')
                                    }
                                }
                            }
                        )
                    } else if (repos.isEmpty()) {
                        repos.add(repos.gradlePluginPortal())
                    }
                    settings.buildscript.dependencies {
                        classpath("$pluginId:${pluginId}.gradle.plugin:$multiCacheVersion")
                    }
                }
            }

            settingsEvaluated { settings ->
                settings.buildCache {
                    boolean needMulticache = remote != null
                    if (needMulticache && !multiCacheEnabled) {
                        println("$pluginId is disabled")
                        return
                    }

                    local {
                        enabled = true
                        push = $push
                    }
                    if (needMulticache) {
                        settings.pluginManager.apply("$pluginId")
                        settings.multicache.push('base')
                    }
                    remote(HttpBuildCache) {
                        url = '$cacheUrl'
                        push = $push
                        // Build cache is located on localhost, so it is fine to use http protocol
                        if (gradle6Plus) {
                            allowInsecureProtocol = true
                        }
                    }
                    if (needMulticache) {
                        settings.multicache.pushAndConfigure('actions-cache') {
                            loadSequentiallyWriteConcurrently('actions-cache', 'base')
                        }
                    }
                }
            }
        """.trimIndent()
    }

    suspend fun start() {
        suspendCoroutine<Nothing?> { cont ->
            server.listen(0) {
                cont.resume(null)
            }
        }

        mkdir(TEMP_DIR)
        val url = "http://localhost:${(server.address().unsafeCast<AddressInfo>()).port}/"
        _cacheUrl = url
        process.env[GHA_CACHE_URL] = url
    }

    fun stop() {
        server.close()
    }

    suspend inline operator fun <T> invoke(block: () -> T): T {
        start()
        try {
            return block()
        } finally {
            stop()
        }
    }
}


================================================
FILE: cache-proxy/src/jsTest/kotlin/com/github/burrunan/gradle/proxy/CacheProxyTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.gradle.proxy

import actions.exec.ExecOptions
import actions.exec.exec
import actions.glob.removeFiles
import com.github.burrunan.gradle.cache.CacheService
import com.github.burrunan.test.runTest
import com.github.burrunan.wrappers.nodejs.mkdir
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToDynamic
import node.buffer.BufferEncoding
import node.fs.copyFile
import node.fs.writeFile
import node.process.process
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.fail

class CacheProxyTest {
    // Emulates Azure Cache Backend for @actions/cache
    val cacheService = CacheService()

    // Implements Gradle HTTP Build Cache via @actions/cache
    val cacheProxy = CacheProxy()

    @Test
    fun abc() = runTest {
        val z = mapOf("a" to 4, "b" to 6)
        println("json: " + JSON.stringify(Json.encodeToDynamic(z)))
    }

    @Test
    fun cacheProxyWorks() = runTest {
        val dir = "remote_cache_test"
        mkdir(dir)
        val root = process.cwd() + "/../../../.."
        console.log(root)
        cacheService {
            cacheProxy {
                val outputFile = "build/out.txt"
                removeFiles(listOf("$dir/$outputFile"))
                copyFile("$root/gradlew", dir + "/gradlew")
                mkdir("$dir/gradle")
                mkdir("$dir/gradle/wrapper")
                copyFile("$root/gradle/wrapper/gradle-wrapper.jar", "$dir/gradle/wrapper/gradle-wrapper.jar")
                copyFile("$root/gradle/wrapper/gradle-wrapper.properties", "$dir/gradle/wrapper/gradle-wrapper.properties")
                writeFile(
                    "$dir/settings.gradle",
                    """
                        rootProject.name = 'sample'
                        boolean gradle6Plus = org.gradle.util.GradleVersion.current() >= org.gradle.util.GradleVersion.version('6.0')
                        buildCache {
                            local {
                                // Only remote cache should be used
                                enabled = false
                            }
                            remote(HttpBuildCache) {
                                url = '${process.env["GHA_CACHE_URL"]}'
                                push = true
                                if (gradle6Plus) {
                                    allowInsecureProtocol = true
                                }
                            }
                        }
                    """.trimIndent(),
                    BufferEncoding.utf8,
                )
                writeFile(
                    "$dir/build.gradle",
                    """
                        tasks.create('props', WriteProperties) {
                          outputFile = file("$outputFile")
                          property("hello", "world")
                        }
                        tasks.create('props2', WriteProperties) {
                          outputFile = file("${outputFile}2")
                          property("hello", "world2")
                        }
                    """.trimIndent(),
                    BufferEncoding.utf8,
                )
                writeFile(
                    "$dir/gradle.properties",
                    """
                    org.gradle.caching=true
                    #org.gradle.caching.debug=true
                    org.gradle.configuration-cache=true
                    """.trimIndent(),
                    BufferEncoding.utf8,
                )
                val out = exec("./gradlew", "props", "-i", "--build-cache", captureOutput = true) {
                    ExecOptions.copy(it,
                        cwd = dir,
                        silent = true,
                        ignoreReturnCode = true,
                    )
                }
                if (out.exitCode != 0) {
                    fail("Unable to execute :props task: STDOUT: ${out.stdout}, STDERR: ${out.stderr}")
                }
                assertContains(
                    out.stdout,
                    "1 actionable task: 1 executed",
                )

                removeFiles(listOf("$dir/$outputFile"))
                val out2 = exec("./gradlew", "props", "-i", "--build-cache", captureOutput = true) {
                    ExecOptions.copy(it,
                        cwd = dir,
                        silent = true,
                        ignoreReturnCode = true,
                    )
                }
                if (out.exitCode != 0) {
                    fail("Unable to execute :props task: STDOUT: ${out.stdout}, STDERR: ${out.stderr}")
                }
                assertContains(out2.stdout, "1 actionable task: 1 from cache")
            }
        }
    }
}


================================================
FILE: cache-service-mock/build.gradle.kts
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

kotlin {
    sourceSets {
        jsMain {
            dependencies {
                implementation(projects.wrappers.actionsCache)
                implementation(projects.wrappers.actionsToolkit)
                implementation(projects.wrappers.nodejs)
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-core")
            }
        }
    }
}


================================================
FILE: cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/CacheService.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import actions.cache.internal.*
import actions.core.debug
import com.github.burrunan.wrappers.js.suspendWithCallback
import com.github.burrunan.wrappers.nodejs.exists
import com.github.burrunan.wrappers.nodejs.readJson
import com.github.burrunan.wrappers.nodejs.readToBuffer
import js.objects.unsafeJso
import node.http.IncomingMessage
import node.http.OutgoingHttpHeaders
import node.http.ServerResponse
import node.net.AddressInfo
import node.process.process
import node.querystring.ParsedUrlQuery
import node.url.Url
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class CacheService {
    companion object {
        const val ARCHIVE_DOWNLOAD_URL = "_apis/artifactcache/get"
    }

    private val storage = CacheStorage()

    private val server = node.http.createServer<IncomingMessage, ServerResponse<*>> { req, res ->
        val query = node.url.parse(req.url!!, true)
        val path = query.pathname ?: ""
        res.handle {
            when {
                path == "/_apis/artifactcache/caches" && req.method == "POST" ->
                    reserveCache(req, res)
                path == "/_apis/artifactcache/cache" && req.method == "GET" ->
                    getCache(query, res)
                path.endsWith(ARCHIVE_DOWNLOAD_URL) && req.method == "GET" ->
                    getContents(query, res)
                path.startsWith("/_apis/artifactcache/caches/") ->
                    cacheOp(path.substringAfter("/_apis/artifactcache/caches/").toInt(), req, res)
                else -> HttpException.notImplemented("Path: $path")
            }
        }
    }

    private fun getContents(query: Url, res: ServerResponse<*>) {
        val key = query.query.unsafeCast<ParsedUrlQuery>()["key"] as String
        val entry = storage.getValue(key)
        res.writeHead(
            200, "Ok",
            unsafeJso<OutgoingHttpHeaders> {
                contentLength = entry.value.length
            },
        )
        res.write(entry.value)
    }

    private suspend fun cacheOp(cacheId: Number, req: IncomingMessage, res: ServerResponse<*>) = when (req.method) {
        "PATCH" -> uploadCache(cacheId, req, res)
        "POST" -> commitCache(cacheId, req, res)
        else -> throw HttpException.notImplemented("Unknown method: ${req.method}")
    }

    private fun getCache(query: Url, res: ServerResponse<*>) {
        val request = query.query.unsafeCast<GetCacheParams>()
        var resultKey: String? = null
        var resultEntry: CacheEntry? = null
        for ((index, key) in request.keys.split(',').withIndex()) {
            if (index == 0) {
                val entry = storage[key] ?: continue
                if (entry.version != request.version) {
                    debug("Entry version differs for key $key. Requested: ${request.version}, actual: ${entry.version}")
                }
                resultKey = key
                resultEntry = entry
                break
            }
            val entry = storage.find(key, request.version) ?: continue
            resultKey = entry.key
            resultEntry = entry.value
        }
        if (resultKey == null) {
            throw HttpException.noContent("No entries found")
        }

        res.writeHead(200, "Ok", undefined.unsafeCast<OutgoingHttpHeaders>())

        res.write(
            JSON.stringify(
                unsafeJso<ArtifactCacheEntry> {
                    cacheKey = resultKey
                    scope = "refs/origin/main"
                    creationTime = resultEntry?.creationTime?.toString()
                    archiveLocation =
                        "${process.env["ACTIONS_CACHE_URL"]}$ARCHIVE_DOWNLOAD_URL?key=$resultKey".takeIf { resultEntry != null }
                },
            ),
        )
    }

    private suspend fun uploadCache(cacheId: Number, req: IncomingMessage, res: ServerResponse<*>) {
        val contentRange = req.headers.asDynamic()["content-range"] as String
        val (_, start, end) = contentRange.match("bytes (\\d+)-(\\d+)") ?: arrayOf("", "", "")
        if (start.isEmpty()) {
            throw HttpException.notImplemented("Unknown content-range: $contentRange")
        }
        storage.update(cacheId, start.toInt(), end.toInt(), req.readToBuffer())
        res.writeHead(200, "OK", undefined.unsafeCast<OutgoingHttpHeaders>())
    }

    private suspend fun commitCache(cacheId: Number, req: IncomingMessage, res: ServerResponse<*>) {
        storage.commitCache(cacheId, req.readJson<CommitCacheRequest>().size)
        res.writeHead(200, "OK", undefined.unsafeCast<OutgoingHttpHeaders>())
    }

    private suspend fun reserveCache(req: IncomingMessage, res: ServerResponse<*>) {
        if (req.method != "POST") {
            throw HttpException.badRequest("Expecting POST method, got ${req.method}")
        }
        val request = req.readJson<ReserveCacheRequest>()

        val cacheId = storage.reserveCache(request.key, request.version!!)
            ?: throw HttpException.badRequest("Cache entry already exists")
        res.writeHead(200, "Reserve Cache OK", undefined.unsafeCast<OutgoingHttpHeaders>())
        res.write(
            JSON.stringify(
                unsafeJso<ReserveCacheResponse> {
                    this.cacheId = cacheId
                },
            ),
        )
    }

    suspend inline operator fun <T> invoke(block: () -> T): T {
        start()
        try {
            return block()
        } finally {
            stop()
        }
    }

    suspend fun start() {
        suspendCoroutine<Nothing?> { cont ->
            server.listen(0) {
                cont.resume(null)
            }
        }

        val runnerTemp = "runner_temp"
        if (!exists(runnerTemp)) {
            com.github.burrunan.wrappers.nodejs.mkdir(runnerTemp)
        }

        process.env["ACTIONS_RUNTIME_TOKEN"] = "42"
        process.env["RUNNER_TEMP"] = process.cwd() + "/" + runnerTemp
        process.env["ACTIONS_CACHE_URL"] = "http://localhost:${(server.address().unsafeCast<AddressInfo>()).port}/"
    }

    suspend fun stop() {
        suspendWithCallback { server.close(it) }
    }
}


================================================
FILE: cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/CacheStorage.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import node.buffer.Buffer

class CacheStorage {
    private val storage = mutableMapOf<String, CacheEntry>()
    private val reservations = mutableMapOf<String, CacheReservation>()
    private val caches = mutableMapOf<Number, TemporaryCache>()
    private var nextId = 0

    fun reserveCache(key: String, version: String): Number? {
        if (key in storage || key in reservations) {
            return null
        }
        if (reservations[key]?.version?.equals(version) == false) {
            return null
        }
        nextId += 1
        reservations[key] = CacheReservation(nextId, version)
        caches[nextId] = TemporaryCache(key)
        return nextId
    }

    operator fun set(key: String, value: CacheEntry) {
        storage[key] = value
    }

    operator fun get(key: String) = storage[key]

    fun getValue(key: String) = storage.getValue(key)

    fun find(prefix: String, version: String) =
        storage.filterKeys { it.startsWith(prefix) }
            .filterValues { it.version == version }
            .maxByOrNull { it.value.creationTime.toDouble() }

    fun update(cacheId: Number, start: Int, end: Int, buffer: Buffer<*>) {
        caches.getValue(cacheId).parts.add(UploadPart(start, end, buffer))
    }

    fun commitCache(cacheId: Number, size: Number) {
        val cache = caches.remove(cacheId)
            ?: throw HttpException.noContent("Cache $cacheId is not found")
        val reservation = reservations.remove(cache.key)
            ?: throw HttpException.noContent("Reservation ${cache.key} is not found for cache $cacheId")

        val parts = cache.parts
        val result = if (parts.size == 1 && parts[0].contents.length == size) {
            parts[0].contents
        } else {
            Buffer.alloc(size).also {
                for (part in parts) {
                    part.contents.copy(it, part.start, 0, part.end)
                }
            }
        }
        set(cache.key, CacheEntry(reservation.version, result, cacheId))
    }
}

class CacheEntry(val version: String, val value: Buffer<*>, val creationTime: Number)

class CacheReservation(val number: Number, val version: String)

class TemporaryCache(val key: String) {
    val parts = mutableListOf<UploadPart>()
}

class UploadPart(val start: Int, val end: Int, val contents: Buffer<*>)


================================================
FILE: cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/HttpException.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

class HttpException(val code: Int, message: String) : Throwable(message) {
    companion object {
        fun noContent(message: String) = HttpException(204, message)
        fun notImplemented(message: String) = HttpException(501, message)
        fun notFound(message: String) = HttpException(404, message)
        fun badRequest(message: String) = HttpException(400, message)
    }
}


================================================
FILE: cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/HttpExtensions.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import node.http.OutgoingHttpHeaders
import node.http.ServerResponse

fun ServerResponse<*>.handle(action: suspend CoroutineScope.() -> Unit) =
    GlobalScope.launch {
        try {
            supervisorScope {
                action()
            }
        } catch (e: HttpException) {
            writeHead(e.code, e.message ?: "no message", undefined.unsafeCast<OutgoingHttpHeaders>())
        } catch (e: Throwable) {
            e.printStackTrace()
            writeHead(500, "Error processing ${e.message}", undefined.unsafeCast<OutgoingHttpHeaders>())
        } finally {
            end()
        }
    }


================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


================================================
FILE: gradle-launcher/build.gradle.kts
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

kotlin {
    sourceSets {
        jsMain {
            dependencies {
                implementation(projects.hashing)
                implementation(projects.wrappers.actionsToolkit)
                implementation(projects.wrappers.javaProperties)
                implementation(projects.wrappers.nodejs)
                implementation("org.jetbrains.kotlin-wrappers:kotlin-actions-http-client")
                implementation("org.jetbrains.kotlin-wrappers:kotlin-actions-tool-cache")
                implementation("org.jetbrains.kotlin-wrappers:kotlin-actions-io")
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-core")
            }
        }
    }
}


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleDistribution.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher

data class GradleDistribution(
    val version: String,
    val distributionUrl: String,
    val distributionSha256Sum: String?,
)

suspend fun resolveDistribution(
    versionSpec: String,
    projectPath: String,
    distributionUrl: String? = null,
    distributionSha256Sum: String? = null,
    enableDistributionSha256SumWarning: Boolean = true
): GradleDistribution {
    return if (distributionUrl == null) {
        when (val version = GradleVersion(versionSpec)) {
            is GradleVersion.Official -> version.findUrl()
            is GradleVersion.Dynamic -> version.findUrl()
            is GradleVersion.Wrapper -> findVersionFromWrapper(projectPath, enableDistributionSha256SumWarning)
        }
    } else {
        GradleDistribution(
            version = versionSpec,
            distributionUrl = distributionUrl,
            distributionSha256Sum = distributionSha256Sum ?: "$distributionUrl.sha256",
        )
    }
}


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleInstaller.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher

import actions.core.ActionFailedException
import actions.core.info
import actions.core.warning
import actions.http.client.HttpClient
import actions.http.client.HttpCodes
import actions.io.rmRF
import actions.tool.cache.cacheDir
import actions.tool.cache.downloadTool
import actions.tool.cache.extractZip
import com.github.burrunan.hashing.hashFiles
import com.github.burrunan.wrappers.nodejs.exists
import js.objects.unsafeJso
import js.promise.await
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import node.buffer.BufferEncoding
import node.fs.chmod
import node.fs.readFile
import node.http.OutgoingHttpHeaders
import node.path.path
import node.process.Platform

suspend fun install(distribution: GradleDistribution): String {
    val cachedTool = actions.tool.cache.find("gradle", distribution.version)
    val gradleDir = if (cachedTool.isNotEmpty()) {
        info("Detected Gradle ${distribution.version} at $cachedTool")
        cachedTool
    } else {
        val gradleZip = downloadTool(distribution.distributionUrl)
        distribution.distributionSha256Sum?.let { expectedSha256 ->
            val hash = hashFiles(gradleZip, algorithm = "sha256", includeFileName = false).hash
            if (hash != expectedSha256) {
                throw ActionFailedException(
                    "Checksum mismatch for Gradle ${distribution.version} (${distribution.distributionUrl}). " +
                        "Expected: $expectedSha256, actual: $hash",
                )
            }
        }
        val extractedGradleDir = extractZip(gradleZip)
        cacheDir(path.join(extractedGradleDir, "gradle-${distribution.version}"), "gradle", distribution.version).also {
            GlobalScope.launch {
                // Remove temporary files
                rmRF(gradleZip)
                rmRF(extractedGradleDir)
            }
        }
    }
    return path.join(gradleDir, "bin", if (node.os.platform() == Platform.win32) "gradle.bat" else "gradle").also {
        if (node.os.platform() != Platform.win32) {
            chmod(it, "755".toInt(8))
        }
    }
}

private val HTTP_AGENT =
    unsafeJso<OutgoingHttpHeaders> {
        set("User-Agent", "burrunan/gradle-cache-action")
    }

suspend fun GradleVersion.Official.findUrl(): GradleDistribution {
    val url = "https://services.gradle.org/versions/all"
    val response =
        HttpClient().getJson<Array<GradleVersionResponse>>(url, HTTP_AGENT).await()
    if (response.statusCode.unsafeCast<HttpCodes>() != HttpCodes.OK) {
        throw ActionFailedException("Unable to lookup $url Gradle version: ${response.statusCode}, ${JSON.stringify(response.result)}")
    }
    return response.result?.firstOrNull { it.version == name }?.resolveChecksum()
        ?: throw ActionFailedException("Unable to find Gradle version $name")
}

suspend fun GradleVersion.Dynamic.findUrl(): GradleDistribution {
    val url = "https://services.gradle.org/versions/$apiPath"
    val response = HttpClient().getJson<dynamic>(url, HTTP_AGENT).await()
    if (response.statusCode.unsafeCast<HttpCodes>() != HttpCodes.OK) {
        throw ActionFailedException("Unable to lookup $url Gradle version: ${response.statusCode}, ${JSON.stringify(response.result)}")
    }
    if (response.result?.version != null) {
        return response.result.unsafeCast<GradleVersionResponse>().resolveChecksum()
    }
    if (this is GradleVersion.ReleaseCandidate) {
        return GradleVersion.Current.findUrl()
    }
    throw ActionFailedException("Empty result from $url: ${JSON.stringify(response.result)}")
}

suspend fun GradleVersionResponse.resolveChecksum() =
    GradleDistribution(
        version = version,
        distributionUrl = downloadUrl,
        distributionSha256Sum = HttpClient().get(checksumUrl, HTTP_AGENT).await().readBody().await().trim(),
    )

suspend fun findVersionFromWrapper(projectPath: String, enableDistributionSha256SumWarning: Boolean): GradleDistribution {
    val gradleWrapperProperties = path.join(projectPath, "gradle", "wrapper", "gradle-wrapper.properties")
    if (!exists(gradleWrapperProperties)) {
        warning("Gradle wrapper configuration is not found at ${path.resolve(gradleWrapperProperties)}.\nWill use the current release Gradle version")
        return GradleVersion.Current.findUrl()
    }
    val propString = readFile(gradleWrapperProperties, BufferEncoding.utf8)
    val props = javaproperties.parseString(propString).run {
        getKeys().associateWith { getFirst(it)!! }
    }

    val distributionUrl = props.getValue("distributionUrl")
    val distributionSha256Sum = props["distributionSha256Sum"]

    val version = distributionUrl.substringAfterLast("/")
        .substringAfter("gradle-")
        .removeSuffix("-all.zip")
        .removeSuffix("-bin.zip")
        .removeSuffix(".zip")

    if (enableDistributionSha256SumWarning && distributionSha256Sum == null) {
        warning(
            "distributionSha256Sum is not set in $gradleWrapperProperties.\n" +
                "Please consider adding the checksum, " +
                "see https://docs.gradle.org/current/userguide/gradle_wrapper.html#configuring_checksum_verification",
        )
    }

    return if (distributionUrl.removePrefix("https").removePrefix("http")
            .startsWith("://services.gradle.org/")
    ) {
        // Official release, use shorter version
        //   https://services.gradle.org/distributions-snapshots/gradle-6.7-20200730220045+0000-all.zip
        //   https://services.gradle.org/distributions/gradle-6.6-rc-4-all.zip
        //   https://services.gradle.org/distributions/gradle-6.5.1-all.zip
        if (distributionUrl.endsWith("-bin.zip") && distributionSha256Sum != null) {
            GradleDistribution(version, distributionUrl, distributionSha256Sum)
        } else {
            // Resolve checksum from the official site
            // This would switch to -bin distribution which is smaller
            GradleVersion.Official(version).findUrl()
        }
    } else {
        GradleDistribution(version, distributionUrl, distributionSha256Sum)
    }
}


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleLauncher.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.launcher

import actions.core.AnnotationProperties
import actions.core.ExitCode
import actions.core.setFailed
import actions.core.setOutput
import actions.exec.ExecListeners
import actions.exec.ExecOptions
import actions.exec.exec
import com.github.burrunan.launcher.internal.GradleErrorCollector
import com.github.burrunan.launcher.internal.GradleOutErrorCollector
import node.process.process

class GradleResult(
    val buildScanUrl: String?,
)

suspend fun launchGradle(params: LaunchParams): GradleResult {
    var buildScanUrl: String? = null
    // See https://youtrack.jetbrains.com/issue/KT-41107
    var failureDetected = false
    val errorCollector = GradleErrorCollector()
    val outCollector = GradleOutErrorCollector()

    @Suppress("REDUNDANT_SPREAD_OPERATOR_IN_NAMED_FORM_IN_FUNCTION")
    val result = exec(
        params.gradle,
        args = *(if (params.daemon) emptyList() else listOf("--no-daemon") +
            params.properties.map { "-P${it.key}=${it.value}" } +
            params.arguments).toTypedArray(),
    ) {
        ExecOptions.copy(
            it,
            cwd = params.projectPath,
            ignoreReturnCode = true,
            listeners = ExecListeners(
                stdline = {
                    val str = it.trimEnd()
                    if (str.startsWith("https://gradle.com/s/")) {
                        setOutput("build-scan-url", str)
                        buildScanUrl = str
                    }
                    outCollector.process(str)
                },
                errline = {
                    errorCollector.process(it)
                    outCollector.process(it)
                },
            )
        )
    }
    errorCollector.done()
    outCollector.done()
    for (error in errorCollector.errors + outCollector.errors) {
        failureDetected = true
        val shortFile = error.file
            ?.removePrefix(process.cwd())
        actions.core.error(
            error.message,
            AnnotationProperties(
                file = shortFile,
                startLine = error.line,
                startColumn = error.col,
            ),
        )
    }
    if (failureDetected) {
        process.exitCode = ExitCode.Failure.unsafeCast<Double>()
    }
    if (!failureDetected && result.exitCode != 0) {
        setFailed("Gradle process finished with a non-zero exit code: ${result.exitCode}")
    }
    return GradleResult(buildScanUrl)
}


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleVersion.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher

sealed class GradleVersion(val name: String, unused: Int = 0) {
    companion object {
        val DYNAMIC_VERSIONS = listOf(
            Current,
            ReleaseCandidate,
            Nightly,
            ReleaseNightly,
        )
        val FIXED_VERSIONS = DYNAMIC_VERSIONS + Wrapper
    }

    abstract class Dynamic(label: String, val apiPath: String) : GradleVersion(label)
    class Official(label: String) : GradleVersion(label) {
        override fun toString() = "Official($name)"
    }

    object Current : Dynamic("current", "current") {
        override fun toString() = "Current"
    }

    object ReleaseCandidate : Dynamic("rc", "release-candidate") {
        override fun toString() = "ReleaseCandidate"
    }

    object Nightly : Dynamic("nightly", "nightly") {
        override fun toString() = "Nightly"
    }

    object ReleaseNightly : Dynamic("release-nightly", "release-nightly") {
        override fun toString() = "ReleaseNightly"
    }

    object Wrapper : GradleVersion("wrapper") {
        override fun toString() = "Wrapper"
    }
}

fun GradleVersion(version: String) =
    GradleVersion.FIXED_VERSIONS.firstOrNull { it.name == version }
        ?: GradleVersion.Official(version)


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleVersionResponse.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher

external interface GradleVersionResponse {
    val version: String
    val downloadUrl: String
    val checksumUrl: String
}


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/LaunchParams.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher

class LaunchParams(
    val gradle: String,
    val daemon: Boolean,
    val projectPath: String,
    val arguments: List<String>,
    val properties: Map<String, String>,
)


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/internal/GradleError.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher.internal

class GradleError(
    val message: String,
    val file: String? = null,
    val line: Int? = null,
    val col: Int? = null,
) {
    override fun toString() = "GradleError(line=$line, col=$col, file=$file, message='$message')"
}


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/internal/GradleErrorCollector.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher.internal

private enum class ErrorHeader(val message: String) {
    FAILURE("FAILURE: "),
    WHERE("* Where:"),
    WHAT_WENT_WRONG("* What went wrong:"),
    TRY("* Try:"),
}

private val errorHeaderValues = ErrorHeader.values()

class GradleErrorCollector {
    private val _errors = mutableListOf<GradleError>()
    val errors: List<GradleError> = _errors

    private val sb = StringBuilder()
    private var nextKey: ErrorHeader? = null
    private val data = mutableMapOf<ErrorHeader, String>()

    fun done() {
        if (data.isNotEmpty()) {
            val message = data[ErrorHeader.WHAT_WENT_WRONG] ?: "Unknown error"

            _errors += data[ErrorHeader.WHERE]?.let { location ->
                Regex("^Build file '(.+)' line: (\\d+)$").matchEntire(location)?.let {
                    GradleError(
                        message = message,
                        file = it.groupValues[1],
                        line = it.groupValues[2].toInt(),
                    )
                }
            } ?: GradleError(message)
        }
        data.clear()
        sb.clear()
    }

    fun process(line: String) {
        val str = line.trimEnd()
        if (str.startsWith(ErrorHeader.FAILURE.message)) {
            done()
            data[ErrorHeader.FAILURE] = str.removePrefix(ErrorHeader.FAILURE.message)
            return
        }

        if (str.startsWith("* Get more help") ||
            str.startsWith("BUILD FAILED ")
        ) {
            done()
            nextKey = null
            return
        }

        errorHeaderValues.firstOrNull { str.startsWith(it.message) }?.let {
            nextKey?.let {
                data[it] = sb.toString().trimEnd()
            }
            sb.clear()
            nextKey = it
            return
        }

        if (nextKey != null) {
            sb.appendLine(line)
        }
    }
}


================================================
FILE: gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/internal/GradleOutErrorCollector.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher.internal

// e: /../build.gradle.kts:62:1: Unresolved reference: invalid
private val KOTLIN_COMPILE_ERROR = Regex("^e: (\\S.+?):(\\d+):(?:(\\d+):)? (.+)$")
// [ant:checkstyle] [ERROR] /.../SqlHopTableFunction.java:56:35: ',' is not followed by whitespace. [WhitespaceAfter]
private val CHECKSTYLE_ERROR = Regex("^\\[ant:checkstyle\\] \\[ERROR\\] (\\S.+?):(\\d+):(?:(\\d+):)? (.+) \\[([^\\]]+)\\]\$")
// /.../RelDataType.java:249: error: reference not found
private val JAVA_ERROR = Regex("^(\\S.+?):(\\d+): error: (.+)$")

class GradleOutErrorCollector {
    private val _errors = mutableListOf<GradleError>()
    val errors: List<GradleError> = _errors
    private var taskName: String = "Unknown task"
    private var javaError: MatchResult? = null
    private val javaErrorLines = mutableListOf<String>()

    fun process(line: String) {
        if (line.startsWith("> Task ") ||
            line.startsWith("> Configure")
        ) {
            taskName = line.removePrefix("> ").let { "[$it]" }
        }
        if (line.startsWith("e: ")) {
            // Looks like Kotlin error
            // e: /../build.gradle.kts:62:1: Unresolved reference: invalid
            KOTLIN_COMPILE_ERROR.matchEntire(line)?.let {
                _errors += GradleError(
                    message = "$taskName ${it.groupValues[4]}",
                    file = it.groupValues[1],
                    line = it.groupValues[2].toInt(),
                    col = it.groupValues[3].takeIf { it.isNotBlank() }?.toInt(),
                )
            }
            return
        }
        // Checkstyle error:
        // [ant:checkstyle] [ERROR] /.../SqlHopTableFunction.java:56:35: ',' is not followed by whitespace. [WhitespaceAfter]
        CHECKSTYLE_ERROR.matchEntire(line)?.let {
            _errors += GradleError(
                message = "$taskName ${"[${it.groupValues[5]}] ".removePrefix("[] ")}${it.groupValues[4]}",
                file = it.groupValues[1],
                line = it.groupValues[2].toInt(),
                col = it.groupValues[3].takeIf { it.isNotBlank() }?.toInt(),
            )
        }
        processJavaError(line)
    }

    private fun processJavaError(line: String) {
        // /.../RelDataType.java:249: error: reference not found
        JAVA_ERROR.matchEntire(line)?.let {
            done()
            javaError = it
            return
        }
        if (javaError != null) {
            val errorContinuation = line.startsWith(" ")
            if (errorContinuation) {
                javaErrorLines += line
            }
            if (!errorContinuation || javaErrorLines.size >= 3) {
                done()
            }
        }
    }

    fun done() {
        javaError?.let {
            _errors += GradleError(
                message = "$taskName ${it.groupValues[3]}\n${javaErrorLines.joinToString("\n")}",
                file = it.groupValues[1],
                line = it.groupValues[2].toInt(),
            )
        }
        javaError = null
        javaErrorLines.clear()
    }
}


================================================
FILE: gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/PropertiesParserTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher

import kotlin.test.Test
import kotlin.test.assertEquals

class PropertiesParserTest {
    @Test
    fun simpleProperties() {
        assertParse(
            mapOf(
                "a-b" to "1",
                "c" to "34 34",
                "url" to "https://example.com",
            ),
            """
            a-b=1
            url=https\://example.com
            # asfd
              c = 34 34

            """.trimIndent(),
        )
    }

    private fun assertParse(expected: Map<String, String>, value: String) {
        assertEquals(
            expected,
            javaproperties.parseString(value).run {
                getKeys().associateWith { get(it) }
            },
            value,
        )
    }
}


================================================
FILE: gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/RetrieveGradleVersionTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher

import actions.core.info
import actions.tool.cache.findAllVersions
import com.github.burrunan.test.runTest
import kotlin.test.Test

class RetrieveGradleVersionTest {
    @Test
    fun retrieve() = runTest {
        for (version in GradleVersion.DYNAMIC_VERSIONS) {
            val res = version.findUrl()
            println("$version => $res")
        }
    }

    @Test
    fun listTools() = runTest {
        listTool("gradle")
        listTool("Gradle")
        listTool("mvn")
        listTool("Maven")
    }

    private fun listTool(tool: String) {
        info("All $tool versions: ${findAllVersions(tool).joinToString(", ")}")
    }
}


================================================
FILE: gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/internal/GradleErrorCollectorTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher.internal

import kotlin.test.Test
import kotlin.test.assertEquals

class GradleErrorCollectorTest {
    @Test
    fun buildScriptFailure() {
        testCollector(
            """
                GradleError(line=62, col=null, file=/home/runner/work/pgjdbc/pgjdbc/build.gradle.kts, message='Script compilation errors:

                  Line 62: invalid code here
                           ^ Unresolved reference: invalid

                  Line 62: invalid code here
                                        ^ Unresolved reference: here

                2 errors')
            """.trimIndent(),
            """
                * Where:
                Build file '/home/runner/work/pgjdbc/pgjdbc/build.gradle.kts' line: 62

                * What went wrong:
                Script compilation errors:

                  Line 62: invalid code here
                           ^ Unresolved reference: invalid

                  Line 62: invalid code here
                                        ^ Unresolved reference: here

                2 errors

                * Try:
                Run with --stacktrace option to get the stack trace. Run with --debug option to get more log output. Run with --scan to get full insights.

                * Get more help at https://help.gradle.org

                BUILD FAILED in 44s
            """.trimIndent(),
        )
    }

    @Test
    fun noLocation() {
        testCollector(
            """
                GradleError(line=null, col=null, file=null, message='Task 'asdfasdf' not found in root project 'pgjdbc'.')
            """.trimIndent(),
            """
                See https://docs.gradle.org/6.3/userguide/command_line_interface.html#sec:command_line_warnings

                FAILURE: Build failed with an exception.

                * What went wrong:
                Task 'asdfasdf' not found in root project 'pgjdbc'.

                * Try:
                Run gradle tasks to get a list of available tasks. Run with --stacktrace option to get the stack trace. Run with --debug option to get more log output. Run with --scan to get full insights.

                * Get more help at https://help.gradle.org

                BUILD FAILED in 37s
            """.trimIndent(),
        )
    }

    private fun testCollector(expected: String, input: String) {
        val collector = GradleErrorCollector()
        input.lines().forEach { collector.process(it) }

        collector.done()

        assertEquals(expected, collector.errors.joinToString("\n"))
    }
}


================================================
FILE: gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/internal/GradleOutCollectorTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.launcher.internal

import kotlin.test.Test
import kotlin.test.assertEquals

class GradleOutCollectorTest {
    @Test
    fun koltlinCompileErrors() {
        testCollector(
            """
                GradleError(line=62, col=1, file=/home/runner/work/pgjdbc/pgjdbc/build.gradle.kts, message='[Configure project :] Unresolved reference: invalid')
                GradleError(line=62, col=14, file=/home/runner/work/pgjdbc/pgjdbc/build.gradle.kts, message='[Configure project :] Unresolved reference: here')
            """.trimIndent(),
            """
                > Configure project :
                Evaluating root project 'pgjdbc' using build file '/home/runner/work/pgjdbc/pgjdbc/build.gradle.kts'.
                Loading cache entry 'cache/eila5i6e0q7sxpvg89w345ymz' from S3 bucket
                e: /home/runner/work/pgjdbc/pgjdbc/build.gradle.kts:62:1: Unresolved reference: invalid
                e: /home/runner/work/pgjdbc/pgjdbc/build.gradle.kts:62:14: Unresolved reference: here
            """.trimIndent(),
        )
    }

    @Test
    fun checkstyleError() {
        testCollector(
            """
                GradleError(line=56, col=35, file=/Users/runner/work/calcite/calcite/core/src/main/java/org/apache/calcite/sql/SqlHopTableFunction.java, message='[Task :core:checkstyleMain] [WhitespaceAfter] ',' is not followed by whitespace.')
                GradleError(line=32, col=null, file=/code/calcite/linq4j/src/main/java/org/apache/calcite/linq4j/AbstractEnumerable.java, message='[Task :core:checkstyleMain] [Indentation] 'method def modifier' has incorrect indentation level 0, expected level should be 2.')
            """.trimIndent(),
            """
                > Task :core:checkstyleMain
                [ant:checkstyle] [ERROR] /Users/runner/work/calcite/calcite/core/src/main/java/org/apache/calcite/sql/SqlHopTableFunction.java:56:35: ',' is not followed by whitespace. [WhitespaceAfter]
                [ant:checkstyle] [ERROR] /code/calcite/linq4j/src/main/java/org/apache/calcite/linq4j/AbstractEnumerable.java:32: 'method def modifier' has incorrect indentation level 0, expected level should be 2. [Indentation]
            """.trimIndent(),
        )
    }

    @Test
    fun javadocError() {
        testCollector(
            """
                GradleError(line=249, col=null, file=/Users/../type/RelDataType.java, message='[Task :babel:javadoc] reference not found
                   * {@link #equals(Object)}.
                            ^')
            """.trimIndent(),
            """
                > Task :babel:javadoc
                /Users/runner/runners/2.263.0/work/calcite/calcite/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java:632: warning: no @param for rel
                  public List<Double> getAverageColumnSizesNotNull(RelNode rel) {
                                      ^
                /Users/runner/runners/2.263.0/work/calcite/calcite/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java:632: warning: no @return
                  public List<Double> getAverageColumnSizesNotNull(RelNode rel) {
                                      ^
                /Users/../type/RelDataType.java:249: error: reference not found
                   * {@link #equals(Object)}.
                            ^
            """.trimIndent(),
        )
    }

    @Test
    fun javacError() {
        testCollector(
            """
                GradleError(line=46, col=null, file=/home/runner/../ReaderInputStreamTest.java, message='[Task :compileJava] cannot find symbol
                    Arrays.fill(acutal, (byte) 0x00);
                                ^
                  symbol:   variable acutal')
            """.trimIndent(),
            """
                > Task :compileJava
                Compiling with JDK Java compiler API.
                /home/runner/../ReaderInputStreamTest.java:46: error: cannot find symbol
                    Arrays.fill(acutal, (byte) 0x00);
                                ^
                  symbol:   variable acutal
                  location: class ReaderInputStreamTest
                Note: Some input files use or override a deprecated API.
                Note: Recompile with -Xlint:deprecation for details.
            """.trimIndent(),
        )
    }

    private fun testCollector(expected: String, input: String) {
        val collector = GradleOutErrorCollector()
        input.lines().forEach { collector.process(it) }

        collector.done()

        assertEquals(expected, collector.errors.joinToString("\n"))
    }
}


================================================
FILE: gradle.properties
================================================
#
# Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

org.gradle.parallel=true

kotlin.daemon.jvmargs=-Xmx1500m
kotlin.parallel.tasks.in.project=true

kotlin.code.style=official


================================================
FILE: gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH="\\\"\\\""


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as '${Hostname}' itself on the command line.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:execute
@rem Setup the command line

set CLASSPATH=


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: hashing/build.gradle.kts
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
plugins {
    kotlin("plugin.serialization")
}

kotlin {
    sourceSets {
        jsMain {
            dependencies {
                implementation(projects.wrappers.actionsToolkit)
                implementation(projects.wrappers.js)
                implementation(projects.wrappers.nodejs)
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-core")
            }
        }
    }
}


================================================
FILE: hashing/src/jsMain/kotlin/com/github/burrunan/hashing/HashDetails.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.hashing

import actions.core.ActionFailedException
import actions.core.warning
import com.github.burrunan.wrappers.nodejs.normalizedPath
import com.github.burrunan.wrappers.nodejs.pipeAndWait
import js.promise.await
import kotlinx.serialization.Serializable
import node.crypto.BinaryToTextEncoding
import node.crypto.createHash
import node.fs.createReadStream
import node.fs.stat
import node.process.process

@Serializable
class HashDetails(
    val info: HashInfo,
    val contents: HashContents,
)

@Serializable
class HashInfo(
    val totalBytes: Long,
    val hash: String,
    val totalFiles: Int,
)

@Serializable
class HashContents(
    val files: Map<String, FileDetails>,
)

@Serializable
class FileDetails(
    val fileSize: Long,
    val hash: String,
)

/**
 * Sample: ~/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.0.18/5f65affce1684999e2f4024983835efc3504012e/error_prone_annotations-2.0.18.jar
 */
private fun sha1FromModulesFileName(key: String): String {
    val lastSlash = key.lastIndexOf('/')
    val hashStart = key.lastIndexOf('/', startIndex = lastSlash - 1) + 1
    return key.substring(hashStart, lastSlash).padStart(40, '0')
}

suspend fun hashFilesDetailed(
    vararg paths: String,
    algorithm: String = "sha1",
    includeFileName: Boolean = true,
): HashDetails = try {
    val globber = actions.glob.create(paths.joinToString("\n"))
    val fileNames = globber.glob().await()
    // Sorting is needed for stable overall hash
    fileNames.sort()

    val githubWorkspace = process.cwd()
    val homeDir = "~".normalizedPath

    var totalBytes = 0L
    val files = mutableMapOf<String, FileDetails>()
    val overallHash = createHash(algorithm)
    for (name in fileNames) {
        val statSync = stat(name)
        if (statSync.isDirectory()) {
            continue
        }
        val fileSize = statSync.size.toLong()
        totalBytes += fileSize
        val key = when {
            name.startsWith(githubWorkspace) ->
                "ws://" + name.substring(githubWorkspace.length)
            name.startsWith(homeDir) ->
                "~" + name.substring(homeDir.length)
            else -> name
        }.replace('\\', '/')
        // Avoid hashing the contents when we know the hash from the file path
        val digest = when {
            algorithm == "sha1" && key.startsWith("~/.gradle/caches/modules-2/files-2.1/") ->
                sha1FromModulesFileName(key)
            key.startsWith("~/.gradle/caches/build-cache-1/") ->
                key.substringAfterLast('/')
            else -> {
                val hash = createHash(algorithm)
                try {
                    createReadStream(name).pipeAndWait(hash, end = true)
                } catch (e: Throwable) {
                    warning("Unable to hash $name, will ignore the file: ${e.stackTraceToString()}")
                    continue
                }
                hash.digest(BinaryToTextEncoding.hex)
            }
        }
        files[key] = FileDetails(fileSize, digest)
        // Add filename
        if (includeFileName) {
            overallHash.update(key)
        }
        overallHash.update(digest)
    }
    HashDetails(
        HashInfo(totalBytes, overallHash.digest(BinaryToTextEncoding.hex), files.size),
        HashContents(files),
    )
} catch (e: Throwable) {
    throw ActionFailedException("Unable to hash ${paths.joinToString(", ")}: $e", e)
}


================================================
FILE: hashing/src/jsMain/kotlin/com/github/burrunan/hashing/diff.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.hashing

import com.github.burrunan.formatBytes

class Diff(
    val newFiles: Int,
    val totalUpdated: Long,
    val totalDeleted: Long,
    val messages: List<String>,
    val updatedFiles: List<String>,
    val deletedFiles: List<String>,
) {
    val summary: String
        get() =
            (if (updatedFiles.isNotEmpty()) "${updatedFiles.size} updates (${totalUpdated.formatBytes()})" else "") +
                (if (deletedFiles.isNotEmpty()) (if (updatedFiles.isNotEmpty()) ", " else "") +
                    "${deletedFiles.size} deletes (${totalDeleted.formatBytes()})" else "") +
                "\n  " + messages.joinToString("\n  ")
}

fun diff(
    oldContents: HashContents,
    newContents: HashContents,
    maxUpdatesToPrint: Int = 50,
): Diff {
    val messages = mutableListOf<String>()
    val updatedFiles = mutableListOf<String>()
    val deletedFiles = mutableListOf<String>()
    var newFiles = 0
    var totalUpdated = 0L
    var totalDeleted = 0L
    for ((file, hash) in newContents.files) {
        val oldHash = oldContents.files[file]
        if (oldHash?.hash == hash.hash) {
            continue
        }
        updatedFiles.add(file)
        if (messages.size >= maxUpdatesToPrint) {
            continue
        }
        messages.add(
            if (oldHash == null) {
                newFiles += 1
                totalUpdated += hash.fileSize
                "N ${hash.fileSize} $file ${hash.hash}"
            } else {
                totalUpdated += hash.fileSize
                "U ${hash.fileSize} $file ${oldHash.fileSize} ${oldHash.hash} => ${hash.hash}"
            },
        )
    }
    if (oldContents.files.size + newFiles != newContents.files.size) {
        for ((file, hash) in oldContents.files) {
            if (file !in newContents.files) {
                deletedFiles.add(file)
                totalDeleted += hash.fileSize
                if (messages.size < maxUpdatesToPrint) {
                    messages.add("D $file $hash")
                }
            }
        }
    }
    return Diff(
        newFiles = newFiles,
        totalUpdated = totalUpdated,
        totalDeleted = totalDeleted,
        messages = messages,
        updatedFiles = updatedFiles,
        deletedFiles = deletedFiles,
    )
}


================================================
FILE: hashing/src/jsMain/kotlin/com/github/burrunan/hashing/hashFiles.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.hashing

import actions.core.ActionFailedException
import actions.core.warning
import com.github.burrunan.wrappers.nodejs.normalizedPath
import com.github.burrunan.wrappers.nodejs.pipeAndWait
import js.promise.await
import node.WritableStream
import node.buffer.BufferEncoding
import node.crypto.BinaryToTextEncoding
import node.crypto.createHash
import node.fs.createReadStream
import node.fs.stat
import node.process.process

data class HashResult(
    val hash: String,
    val numFiles: Int,
    val totalBytes: Int,
)

suspend fun hashFiles(
    vararg paths: String,
    algorithm: String = "sha1",
    includeFileName: Boolean = true,
): HashResult = try {
    val globber = actions.glob.create(paths.joinToString("\n"))
    val fileNames = globber.glob().await()
    fileNames.sort()

    val githubWorkspace = process.cwd()
    val homeDir = "~".normalizedPath
    val hash = createHash(algorithm)

    var totalBytes = 0
    var numFiles = 0
    for (name in fileNames) {
        val statSync = stat(name)
        if (statSync.isDirectory()) {
            continue
        }
        val key = when {
            name.startsWith(githubWorkspace) ->
                "ws://" + name.substring(githubWorkspace.length)
            name.startsWith(homeDir) ->
                "~" + name.substring(homeDir.length)
            else -> name
        }.replace('\\', '/')

        numFiles += 1
        totalBytes += statSync.size.toInt()
        // Add filename

        try {
            createReadStream(name).pipeAndWait(hash, end = false)
        } catch (e: Throwable) {
            warning("Unable to hash $name, will ignore the file: ${e.stackTraceToString()}")
            continue
        }

        if (includeFileName) {
            hash.update(key, BufferEncoding.utf8)
        }
    }
    hash.unsafeCast<WritableStream>().end()
    HashResult(
        hash = hash.digest(BinaryToTextEncoding.hex),
        numFiles = numFiles,
        totalBytes = totalBytes,
    )
} catch (e: Throwable) {
    throw ActionFailedException("Unable to hash ${paths.joinToString(", ")}: $e", e)
}


================================================
FILE: layered-cache/build.gradle.kts
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

plugins {
    kotlin("plugin.serialization")
}

kotlin {
    sourceSets {
        jsMain {
            dependencies {
                implementation(projects.gradleLauncher)
                implementation(projects.hashing)
                implementation(projects.wrappers.actionsCache)
                implementation(projects.wrappers.actionsToolkit)
                implementation(projects.wrappers.nodejs)
                implementation(projects.wrappers.octokitWebhooks)
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-core")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
            }
        }
        jsTest {
            dependencies {
                implementation(projects.cacheServiceMock)
            }
        }
    }
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/GradleCacheAction.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle

import actions.core.ActionFailedException
import actions.core.ActionStage
import actions.core.info
import actions.exec.exec
import com.github.burrunan.gradle.cache.*
import com.github.burrunan.gradle.github.suspendingStateVariable
import com.github.burrunan.launcher.GradleDistribution
import octokit.ActionsTrigger
import kotlin.js.Date
import kotlin.math.roundToInt

class GradleCacheAction(
    val trigger: ActionsTrigger,
    val params: Parameters,
    val gradleDistribution: GradleDistribution,
) {
    companion object {
        const val DEFAULT_BRANCH_VAR = "defaultbranch"
    }

    private val treeId = suspendingStateVariable("tree_id") {
        // Sometimes the output might include a newline, so trim it
        // See https://github.com/burrunan/gradle-cache-action/issues/63
        exec("git", "log", "-1", "--quiet", "--format=%T", captureOutput = true).stdout.trim()
    }

    suspend fun execute(stage: ActionStage) {
        val gradleVersion = gradleDistribution.version

        val caches = mutableListOf<Cache>()

        if (params.generatedGradleJars) {
            caches.add(gradleGeneratedJarsCache(gradleVersion))
        }

        if (params.localBuildCache) {
            caches.add(localBuildCache(params.jobId, trigger, gradleVersion, treeId.get()))
        }

        if (params.gradleDependenciesCache) {
            caches.add(gradleDependenciesCache(trigger, params.path, params.gradleDependenciesCacheKey))
        }

        if (params.mavenDependenciesCache) {
            caches.add(mavenDependenciesCache(trigger, params.path, params.mavenLocalIgnorePaths))
        }

        val cache = CompositeCache("all-caches", caches, concurrent = params.concurrent)
        when (stage) {
            ActionStage.MAIN -> {
                val started = Date.now()
                val restore = cache.restore()
                val elapsed = Date.now() - started
                info("Cache restore took ${(elapsed / 1000).roundToInt()} seconds")
            }
            ActionStage.POST -> {
                if (params.readOnly) {
                    info("read-only == true, so will skip cache upload")
                } else {
                    cache.save()
                }
            }
            else -> throw ActionFailedException("Cache action should be called in PRE or POST stages only. " +
                "Current stage is $stage")
        }
    }
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/Parameters.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle

data class Parameters(
    val jobId: String,
    val path: String,
    val debug: Boolean,
    val generatedGradleJars: Boolean,
    val localBuildCache: Boolean,
    val gradleDependenciesCache: Boolean,
    val gradleDependenciesCacheKey: List<String>,
    val mavenDependenciesCache: Boolean,
    val mavenLocalIgnorePaths: List<String>,
    val concurrent: Boolean,
    val readOnly: Boolean,
)


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/ActionsTriggerExtensions.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.gradle.cache

import actions.core.ActionsEnvironment
import com.github.burrunan.gradle.GradleCacheAction
import octokit.ActionsTrigger

val ActionsTrigger.cacheKey: String
    get() = when (this) {
        is ActionsTrigger.PullRequest -> "PR${event.pull_request.number}"
        is ActionsTrigger.BranchPush -> when (val ref = event.ref.removePrefix("refs/heads/")) {
            event.repository.default_branch.removePrefix("refs/heads/") ->
                GradleCacheAction.DEFAULT_BRANCH_VAR
            else -> ref
        }
        is ActionsTrigger.Schedule, is ActionsTrigger.WorkflowDispatch ->
            GradleCacheAction.DEFAULT_BRANCH_VAR
        is ActionsTrigger.Other -> "$name-${ActionsEnvironment.GITHUB_WORKFLOW}-${ActionsEnvironment.GITHUB_SHA}"
    }


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/Cache.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import actions.cache.RestoreType

interface Cache {
    val name: String

    suspend fun save()
    suspend fun restore(): RestoreType
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/CompositeCache.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import actions.cache.RestoreType
import actions.core.ext.group
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope

class CompositeCache(
    override val name: String,
    private val caches: List<Cache>,
    private val concurrent: Boolean,
) : Cache {
    override suspend fun save() {
        if (!concurrent) {
            for (cache in caches) {
                group("Save ${cache.name}") {
                    cache.save()
                }
            }
            return
        }

        supervisorScope {
            for (cache in caches) {
                launch {
                    cache.save()
                }
            }
        }
    }

    override suspend fun restore(): RestoreType {
        if (!concurrent) {
            for (cache in caches) {
                group("Restore ${cache.name}") {
                    cache.restore()
                }
            }
            return RestoreType.Unknown
        }

        supervisorScope {
            for (cache in caches) {
                launch {
                    cache.restore()
                }
            }
        }
        return RestoreType.Unknown
    }
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/DefaultCache.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import actions.cache.RestoreType
import actions.cache.restoreAndLog
import actions.cache.saveAndLog
import actions.core.debug
import actions.core.info
import actions.glob.removeFiles
import com.github.burrunan.formatBytes
import com.github.burrunan.gradle.github.stateVariable
import com.github.burrunan.gradle.github.toBoolean
import com.github.burrunan.gradle.github.toInt
import com.github.burrunan.hashing.HashContents
import com.github.burrunan.hashing.HashDetails
import com.github.burrunan.hashing.HashInfo
import com.github.burrunan.hashing.hashFilesDetailed
import com.github.burrunan.wrappers.nodejs.exists
import kotlin.math.absoluteValue

class DefaultCache(
    name: String,
    val primaryKey: String,
    private val restoreKeys: List<String> = listOf(),
    private val paths: List<String>,
    private val readOnlyMessage: String? = null,
    stateKey: String = "",
    private val skipRestoreIfPathExists: String? = null
) : Cache {
    @Suppress("CanBePrimaryConstructorProperty")
    override val name: String = name

    private val version = "1-"

    private val cacheInfo =
        MetadataFile("$name-info", HashInfo.serializer())
    private val cacheContents =
        MetadataFile("$name-contents", HashContents.serializer())
    private val saveRestorePaths = paths + cacheInfo.cachedName + cacheContents.cachedName

    private val isExactMatch = stateVariable("${name}_${stateKey}_exact").toBoolean()
    private val isSkipped = stateVariable("${name}_${stateKey}_skip").toBoolean()
    private val restoredKeyIndex = stateVariable("${name}_${stateKey}_key").toInt(-1)

    private val restoredKey: String?
        get() = when {
            isExactMatch.get() -> primaryKey
            restoredKeyIndex.get() >= 0 -> restoreKeys[restoredKeyIndex.get()]
            else -> null
        }

    private var details: HashDetails? = null

    suspend fun info(): HashInfo? {
        details?.info?.let { return it }
        restoredKey?.let { cacheContents.prepare(it) }
        if (!isExactMatch.get() && restoredKeyIndex.get() == -1) {
            // Cache was not restored => no information known
            return null
        }
        return cacheInfo.decode()
    }

    suspend fun contents(): HashContents? {
        details?.contents?.let { return it }
        restoredKey?.let { cacheContents.prepare(it) }
        if (!isExactMatch.get() && restoredKeyIndex.get() == -1) {
            // Cache was not restored => no information known
            return null
        }
        return cacheContents.decode()
    }

    override suspend fun restore(): RestoreType {
        skipRestoreIfPathExists?.let {
            if (exists(it)) {
                debug { "$name: $it already exists, so the cache restore and upload will be skipped" }
                isSkipped.set(true)
            }
        }
        debug { "$name: restoring $primaryKey, $restoreKeys, $saveRestorePaths" }
        return restoreAndLog(saveRestorePaths, primaryKey, restoreKeys, version = version).also {
            isExactMatch.set(it is RestoreType.Exact)
            restoredKeyIndex.set(
                when (it) {
                    is RestoreType.Partial -> restoreKeys.indexOfFirst { key -> it.path.startsWith(key) }
                    else -> -1
                },
            )
            debug { "$name: restore type $it, ${isExactMatch.get()}, ${restoredKeyIndex.get()}" }

            restoredKey?.let { key ->
                cacheInfo.restore(key)
                cacheContents.restore(key)
            }
        }
    }

    override suspend fun save() {
        debug { "$name: saving ${isExactMatch.get()} ${restoredKeyIndex.get()} $primaryKey, $restoreKeys, $saveRestorePaths" }
        if (isSkipped.get()) {
            debug { "$name: cache save skipped" }
            return
        }
        if (isExactMatch.get()) {
            info("$name loaded from exact match, no need to update the cache entry")
            return
        }
        readOnlyMessage?.let {
            info("$name is configured as read-only: $it")
            return
        }

        restoredKey?.let { key ->
            cacheInfo.prepare(key)
            cacheContents.prepare(key)
        }

        val oldHash = info()

        val newHash = hashFilesDetailed(*paths.toTypedArray())
        details = newHash

        if (newHash.contents.files.isEmpty()) {
            info("$name: no files to cache => won't upload empty cache")
            return
        }
        if (oldHash != null) {
            info("$name: comparing modifications of the cache contents")
            if (newHash.info.hash == oldHash.hash) {
                info("$name: contents did not change => no need to upload it")
                return
            }
            val delta = newHash.info.totalBytes - oldHash.totalBytes
            info("$name: hash content differs (${delta.absoluteValue} bytes ${if (delta >= 0) "increase" else "decrease"})")
        }
        info("$name: uploading ${newHash.info.totalBytes.formatBytes()}, ${newHash.contents.files.size} files as $primaryKey")
        cacheInfo.encode(newHash.info)
        cacheContents.encode(newHash.contents)
        saveAndLog(saveRestorePaths, primaryKey, version = version)
    }
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/GradleGeneratedJarsCache.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

fun gradleGeneratedJarsCache(gradleVersion: String): Cache =
    DefaultCache(
        name = "gradle-generated-jars",
        primaryKey = "generated-gradle-jars-gradle-$gradleVersion",
        paths = listOf(
            "~/.gradle/caches/$gradleVersion/generated-gradle-jars/*",
            "!~/.gradle/caches/$gradleVersion/generated-gradle-jars/*.lock",
        ),
        skipRestoreIfPathExists = "~/.gradle/caches/$gradleVersion/generated-gradle-jars",
    )


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/LayeredCache.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import actions.cache.RestoreType
import actions.core.ActionFailedException
import actions.core.debug
import actions.core.info
import actions.core.warning
import actions.glob.removeFiles
import com.github.burrunan.formatBytes
import com.github.burrunan.gradle.github.stateVariable
import com.github.burrunan.gradle.github.toBoolean
import com.github.burrunan.hashing.*
import kotlinx.serialization.Serializable

@Serializable
class CacheLayers(
    val layers: List<CacheLayer>,
    val deletedFiles: List<String>,
)

@Serializable
class CacheLayer(
    // format is $version-...
    val primaryKey: String,
    val paths: List<String>,
)

class LayeredCache(
    override val name: String,
    private val baseline: String,
    val maxLayers: Int = 5,
    private val primaryKey: String,
    private val restoreKeys: List<String> = listOf(),
    private val paths: List<String>,
) : Cache {
    private val version = "1"

    private val layers = MetadataFile("layer-$name", CacheLayers.serializer())

    private val isExactMatch = stateVariable("${name}_exact").toBoolean()

    private val index = DefaultCache(
        name = "$version-index-$name",
        primaryKey = "$version-index-$primaryKey",
        restoreKeys = restoreKeys.map { "$version-index-$it" },
        paths = listOf(layers.cachedName),
    )

    override fun toString(): String =
        "Cache $name, primaryKey=$primaryKey, restoreKeys=$restoreKeys, "

    private fun CacheLayer.toCache(stateKey: String) =
        DefaultCache(
            name = name,
            stateKey = stateKey,
            primaryKey = primaryKey,
            restoreKeys = if (paths.isNotEmpty()) listOf() else restoreKeys.map { "$version-$it" },
            paths = this@toCache.paths.ifEmpty { this@LayeredCache.paths },
        )

    private fun Diff.toLayer(): CacheLayer {
        // @actions/cache treats "paths" as a part of the cache key, so "delta-" is not important here for correctness
        // delta- is here for readability
        return CacheLayer(
            primaryKey = "$version-delta-$primaryKey",
            paths = updatedFiles,
        )
    }

    override suspend fun restore(): RestoreType {
        val indexRestoreType = index.restore()
        if (indexRestoreType == RestoreType.None) {
            return RestoreType.None
        }
        val cacheIndex = layers.decode() ?: run {
            warning("Unable to restore cache $this")
            return RestoreType.Unknown
        }

        var restoreType: RestoreType = when (indexRestoreType) {
            is RestoreType.Exact -> RestoreType.Exact(indexRestoreType.path.removePrefix("$version-index-"))
            is RestoreType.Partial -> RestoreType.Partial(indexRestoreType.path.removePrefix("$version-index-"))
            else -> indexRestoreType
        }

        info(
            cacheIndex.layers.joinToString(", ", "$name: ${cacheIndex.layers.size} layers. ") {
                if (it.paths.isEmpty()) it.primaryKey else "${it.primaryKey} (${it.paths.size} files)"
            },
        )

        // Restore layers one by one, so newer layers can overwrite the old files
        for ((index, layer) in cacheIndex.layers.withIndex()) {
            val cache = layer.toCache(index.toString())
            val restore = cache.restore()
            if (restore !is RestoreType.Exact) {
                restoreType = RestoreType.Unknown
            }
            debug { "$name: layer $index, restore=$restore" }
        }
        removeFiles(cacheIndex.deletedFiles)
        isExactMatch.set(restoreType is RestoreType.Exact)
        return restoreType
    }

    override suspend fun save() {
        if (isExactMatch.get()) {
            info("$name loaded from exact match, no need to update the cache entry")
            return
        }

        val cacheIndex = layers.decode(warnOnMissing = false)
        val isBaseline = primaryKey.startsWith(baseline)
        if (cacheIndex == null) {
            if (!isBaseline) {
                info("$name: old contents is not found, and the current cache $primaryKey does not start with $baseline, so cache saving can't be done")
                return
            }
            saveSingleLayerCache()
            return
        }

        // PR: keep all baseline layers, add one new for PR
        // branch:

        val caches = cacheIndex.layers
            .mapIndexed { index, cacheLayer -> cacheLayer.toCache(index.toString()) }
        val oldContents = caches.associateWith { it.contents() }

        if (isBaseline) {
            val missing = oldContents.mapNotNull { (cache, contents) ->
                if (contents == null) cache.primaryKey else null
            }

            if (missing.isNotEmpty()) {
                info("$name: there are missing layers: $missing")
                saveSingleLayerCache()
                return
            }
            if (cacheIndex.layers.size > maxLayers) {
                info("$name: ${cacheIndex.layers.size} layers reached, will create new snapshot")
                saveSingleLayerCache()
                return
            }
        }

        // non-baseline

        if (!isBaseline) {
            val firstLayer = cacheIndex.layers.firstOrNull()
            val firstBaseline = caches.firstOrNull { it.name.startsWith(baseline) }?.name
            if (firstLayer?.primaryKey?.startsWith("$version-$baseline") != true) {
                info("$name: the first baseline is not found, and the current cache $primaryKey does not start with $version-$baseline, so cache saving can't be done")
                return
            }
            if (oldContents.values.firstOrNull() == null) {
                info("$name: the first baseline $firstBaseline was not received, and the current cache $primaryKey does not start with $baseline, so cache saving can't be done")
                return
            }
        }

        val newContents = hashFilesDetailed(*paths.toTypedArray())

        val oldFiles = mutableMapOf<String, FileDetails>()
        val reusedFiles = mutableMapOf<String, FileDetails>()
        val deletedFiles = mutableSetOf<String>()
        val newLayers = mutableListOf<CacheLayer>()
        val layerInfo = mutableListOf<String>()
        for ((layer, contents) in cacheIndex.layers.zip(oldContents.values)) {
            if (contents == null) {
                info("$name: unknown contents for layer ${layer.primaryKey}")
                continue
            }
            var helpfulBytes = 0L
            var wastedBytes = 0L
            val helpfulLayerFiles = mutableMapOf<String, FileDetails>()
            val deletedLayerFiles = mutableListOf<String>()
            for ((file, details) in contents.files) {
                val newDetails = newContents.contents.files[file]
                if (details.hash == newDetails?.hash && file !in reusedFiles) {
                    // same file => previous layer is helpful
                    helpfulBytes += details.fileSize
                    helpfulLayerFiles[file] = details
                } else {
                    // file is different or removed => previous layer is not useful
                    if (newDetails == null) {
                        deletedLayerFiles.add(file)
                    }
                    wastedBytes += details.fileSize
                }
            }
            if (wastedBytes >= helpfulBytes) {
                // Too much waste => remove the layer
                info("$name: layer ${layer.primaryKey} has too much waste (${wastedBytes.formatBytes()} > ${helpfulBytes.formatBytes()}), so the layer will be skipped")
                continue
            }
            oldFiles += contents.files
            reusedFiles += helpfulLayerFiles
            deletedFiles += deletedLayerFiles
            newLayers += layer
            layerInfo += "${layer.primaryKey} ${(helpfulBytes + wastedBytes).formatBytes()} total (${contents.files.size} files), ${wastedBytes.formatBytes()} outdated"
        }

        if (!isBaseline && newLayers.isEmpty()) {
            info("$name: at least one layer from the default branch is needed. The new contents is ${newContents.info.totalBytes.formatBytes()} (${newContents.info.totalFiles} files)")
            return
        }

        val diff = diff(HashContents(oldFiles), newContents.contents)
        if (diff.messages.isNotEmpty()) {
            info("$name: cache contents is changed: ${diff.summary}")
        }

        val layer = diff.toLayer()
        val cache = layer.toCache("newlayer")
        // TODO: reuse HashDetails when saving cache
        cache.save()

        newLayers += layer
        layerInfo += "${layer.primaryKey} ${diff.totalUpdated.formatBytes()} total (${diff.updatedFiles.size} files), ${diff.totalDeleted.formatBytes()} deleted (${diff.deletedFiles.size} files)"

        info(layerInfo.joinToString("; ", "$name: ${newLayers.size} layers. "))

        layers.encode(CacheLayers(newLayers, deletedFiles = deletedFiles.toList()))
        // Save the index
        index.save()
    }

    private suspend fun saveSingleLayerCache() {
        info("$name: creating single-layer cache image")
        val layer = CacheLayer(
            primaryKey = "$version-$primaryKey",
            paths = listOf(),
        )
        val cache = layer.toCache("single-layer")
        cache.save()
        if (cache.info()?.totalFiles == 0) {
            // cache is empty => skip creating empty file
            return
        }
        layers.encode(CacheLayers(listOf(layer), deletedFiles = listOf()))
        index.save()
    }
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/MetadataFile.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import actions.core.warning
import com.github.burrunan.wrappers.nodejs.exists
import com.github.burrunan.wrappers.nodejs.normalizedPath
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import node.buffer.BufferEncoding
import node.fs.*

class MetadataFile<T>(name: String, private val serializer: KSerializer<T>, private val extension: String = ".json") {
    companion object {
        const val ROOT_FOLDER = "~/.gradle-cache-action"
        val SPECIAL_CHARS = Regex("""[!@#$%^&*:;'"{}\r\n\[\]\\]""")

        init {
            val path = ROOT_FOLDER.normalizedPath
            if (!existsSync(path)) {
                try {
                    mkdirSync(path)
                } catch (ignored: Throwable) {
                }
            }
        }
    }

    val cachedName = "$ROOT_FOLDER/$name$extension"
    private var uniqueName = cachedName.normalizedPath

    fun prepare(key: String) {
        uniqueName = "${cachedName.normalizedPath}.${key.replace('/', '-')}"
    }

    suspend fun restore(key: String) {
        val path = cachedName.normalizedPath
        if (exists(path)) {
            prepare(key)
            rename(path, uniqueName)
        } else {
            warning("$cachedName: $path does not exist")
        }
    }

    suspend fun decode(warnOnMissing: Boolean = true): T? {
        if (!exists(uniqueName)) {
            if (warnOnMissing) {
                warning("$cachedName: $uniqueName does not exist")
            }
            return null
        }
        return try {
            Json.decodeFromString(
                serializer,
                readFile(uniqueName, BufferEncoding.utf8)
            )
        } catch (e: SerializationException) {
            warning("$cachedName: error deserializing $uniqueName with ${serializer.descriptor.serialName}, message: $e")
            return null
        }
    }

    suspend fun encode(value: T) {
        writeFile(
            cachedName.normalizedPath,
            Json.encodeToString(serializer, value),
            BufferEncoding.utf8,
        )
    }
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/dependenciesCache.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.burrunan.gradle.cache

import actions.core.ActionsEnvironment
import actions.core.debug
import com.github.burrunan.gradle.GradleCacheAction
import com.github.burrunan.gradle.github.suspendingStateVariable
import com.github.burrunan.hashing.hashFiles
import octokit.ActionsTrigger

/**
 * Populate cache only when building a default branch, otherwise treat the cache as read-only.
 */
suspend fun dependenciesCache(
    name: String,
    trigger: ActionsTrigger,
    cacheLocation: List<String>,
    pathDependencies: List<String>,
): Cache {
    val defaultBranch = GradleCacheAction.DEFAULT_BRANCH_VAR
    val pkPrefix = trigger.cacheKey
    val cacheName = "dependencies-$name"

    // Avoid re-computing the hash for saving the cache
    val dependencyDeclarationHash = suspendingStateVariable(cacheName) {
        hashFiles(*pathDependencies.toTypedArray()).hash
    }
    debug { "$cacheName: dependencyDeclarationHash=${dependencyDeclarationHash.get()}" }
    val prefix = "dependencies-$name-${ActionsEnvironment.RUNNER_OS}"
    return LayeredCache(
        name = cacheName,
        baseline = prefix,
        primaryKey = "$prefix-$pkPrefix-${dependencyDeclarationHash.get()}",
        restoreKeys = listOf(
            "$prefix-$pkPrefix",
            "$prefix-$defaultBranch",
            "$prefix-master",
            "$prefix-main",
        ),
        paths = cacheLocation,
    )
}

suspend fun gradleDependenciesCache(trigger: ActionsTrigger, path: String, gradleDependenciesCacheKey: List<String>): Cache =
    dependenciesCache(
        "gradle",
        trigger,
        cacheLocation = listOf(
            "~/.gradle/caches/modules-2/*",
            "!~/.gradle/caches/modules-2/gc.properties",
            "!~/.gradle/caches/modules-2/modules-2.lock",
        ),
        pathDependencies = listOf(
            "$path/**/*.gradle",
            "$path/**/*.gradle.kts",
            "$path/**/gradle/dependency-locking/**",
            "$path/**/*.properties",
            "$path/**/gradle/libs.versions.toml",
        ) + gradleDependenciesCacheKey.map {
                (if (it.startsWith("!")) "!" else "") +
                    "$path/**/" + it.trim().trimStart('!')
            } +
            // Excludes must go the last so they win
            listOf("!$path/**/.gradle/"),
    )

suspend fun mavenDependenciesCache(trigger: ActionsTrigger, path: String, mavenLocalIgnorePaths: List<String>): Cache =
    dependenciesCache(
        "maven",
        trigger,
        cacheLocation = listOf("~/.m2/repository") +
            mavenLocalIgnorePaths.map { "!~/.m2/repository/$it" },
        pathDependencies = listOf(
            "$path/**/pom.xml",
        ),
    )


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/localBuildCache.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.cache

import com.github.burrunan.gradle.GradleCacheAction
import octokit.ActionsTrigger

fun localBuildCache(jobId: String, trigger: ActionsTrigger, gradleVersion: String, treeId: String): Cache {
    val buildCacheLocation = "~/.gradle/caches/build-cache-1"
    val defaultBranch = GradleCacheAction.DEFAULT_BRANCH_VAR
    val pkPrefix = trigger.cacheKey

    val restoreKeys = when (trigger) {
        is ActionsTrigger.PullRequest -> arrayOf(
            pkPrefix,
            trigger.event.pull_request.base.ref.removePrefix("refs/heads/"),
        )
        is ActionsTrigger.BranchPush -> arrayOf(
            pkPrefix,
        )
        else -> arrayOf()
    } + arrayOf(
        defaultBranch,
        "master",
        "main",
    )
    val prefix = "gradle-build-cache-$jobId-gradle-$gradleVersion"
    return LayeredCache(
        name = "local-build-cache",
        baseline = "$prefix-$defaultBranch",
        primaryKey = "$prefix-$pkPrefix-$treeId",
        restoreKeys = restoreKeys.map { "$prefix-$it" },
        paths = listOf(
            "$buildCacheLocation/*",
            "!$buildCacheLocation/gc.properties",
            "!$buildCacheLocation/build-cache-1.lock",
        ),
    )
}


================================================
FILE: layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/github/StateExtensions.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.burrunan.gradle.github

import actions.core.getState
import actions.core.saveState

interface MutableStateVariable<T : Any> {
    fun set(value: T)
}

open class BaseStateVariable<T : Any>(protected val name: String) : MutableStateVariable<T> {
    protected var value: T? = null

    override fun set(value: T) {
        this.value = value
        saveState(name, value)
    }
}

interface SuspendingStateVariable<T : Any> : MutableStateVariable<T> {
    suspend fun get(): T
}

class DefaultSuspendingStateVariable(name: String, val default: suspend () -> String) :
    BaseStateVariable<String>(name), SuspendingStateVariable<String> {
    override suspend fun get(): String =
        value ?: getState(name).ifBlank { default() }.also { set(it) }
}

interface StateVariable<T : Any> : MutableStateVariable<T> {
    fun get(): T
}

class DefaultStateVariable(name: String, private val default: () -> String) :
    BaseStateVariable<String>(name), StateVariable<String> {
    override fun get(): String =
        value ?: getState(name).ifBlank { default() }.also { set(it) }
}

fun suspendingStateVariable(name: String, default: suspend () -> String): SuspendingStateVariable<String> =
    DefaultSuspendingStateVariable(name, default)

fun stateVariable(name: String, default: () -> String = { "" }): StateVariable<String> =
    DefaultStateVariable(name, default)

fun <T : Any, R : Any> StateVariable<T>.transform(decode: (T) -> R, encode: (R) -> T) = object : StateVariable<R> {
    override fun get(): R = decode(this@transform.get())

    override fun set(value: R) {
        this@transform.set(encode(value))
    }
}

fun StateVariable<String>.toBoolean() = transform({ it == "Y" }, { if (it) "Y" else "N" })
fun StateVariable<String>.toInt(default: Int) = transform({ if (it.isBlank()) default else it.toInt() }, { it.toString() })
fun StateVariable<String>.toLong(default: Long) = transform({ if (it.isBlank()) default else it.toLong() }, { it.toString() })


================================================
FILE: layered-cache/src/jsTest/kotlin/com/github/burrunan/gradle/CacheServerTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.github.burrunan.gradle

import actions.cache.RestoreType
import actions.cache.restoreAndLog
import actions.cache.saveAndLog
import com.github.burrunan.gradle.cache.CacheService
import com.github.burrunan.gradle.cache.LayeredCache
import com.github.burrunan.test.runTest
import com.github.burrunan.wrappers.nodejs.mkdir
import node.buffer.BufferEncoding
import node.fs.readFile
import node.fs.unlink
import node.fs.writeFile
import kotlin.test.Test
import kotlin.test.assertEquals

class CacheServerTest {
    val cacheService = CacheService()

    @Test
    fun saveCache() = runTest {
        val dir = "saveCache"
        mkdir(dir)
        val file = "$dir/cached.txt"
        val contents = "hello, world"
        writeFile(file, contents, BufferEncoding.utf8)
        val patterns = listOf("$dir/**")

        val primaryKey = "linux-gradle-feature/123123"

        cacheService {
            saveAndLog(patterns, primaryKey, "1-")

            unlink(file)

            assertEquals(
                RestoreType.Exact(primaryKey),
                restoreAndLog(
                    patterns,
                    primaryKey,
                    restoreKeys = listOf("linux-gradle-", "linux-"),
                    version = "1-",
                ),
                "Cache restored from exact match",
            )

            assertEquals(
                readFile(file, BufferEncoding.utf8),
                contents,
                "Contents after restore should match",
            )

            assertEquals(
                RestoreType.Partial(primaryKey),
                restoreAndLog(
                    patterns,
                    "asdf$primaryKey",
                    restoreKeys = listOf("linux-gradle-", "linux-"),
                    version = "1-",
                ),
                "PK not found => restored from restoreKeys",
            )

            assertEquals(
                readFile(file, BufferEncoding.utf8),
                contents,
                "Contents after restore should match",
            )
        }
    }

    @Test
    fun layeredCacheTest() = runTest {
        val dir = "saveCache"
        mkdir(dir)
        val file = "$dir/cached.txt"
        val contents = "hello, world"
        writeFile(file, contents, BufferEncoding.utf8)
        val patterns = listOf("$dir/**")

        val primaryKey = "prefix-gradle-features/cool/123123"

        val cache = LayeredCache(
            "test-cache",
            "prefix-",
            primaryKey = primaryKey,
            restoreKeys = listOf(
                "prefix-gradle-",
                "prefix-",
            ),
            paths = patterns
        )

        cacheService {
            assertEquals(
                RestoreType.None,
                cache.restore(),
                "No data -> RestoreType.None"
            )

            cache.save()

            assertEquals(
                RestoreType.Exact(primaryKey),
                cache.restore(),
                "Restore after saving exact cache -> RestoreType.Exact"
            )
        }
    }
}


================================================
FILE: layered-cache/src/jsTest/kotlin/com/github/burrunan/gradle/GlobTest.kt
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.github.burrunan.gradle

import com.github.burrunan.formatBytes
import com.github.burrunan.hashing.hashFilesDetailed
import com.github.burrunan.test.runTest
import com.github.burrunan.wrappers.nodejs.mkdir
import node.buffer.BufferEncoding
import node.fs.writeFile
import node.path.path
import kotlin.test.Test
import kotlin.test.assertEquals

class GlobTest {
    @Test
    fun glob() = runTest {
        val dirName = "globTest"
        val dotGradle = path.join(dirName, ".gradle")
        mkdir(dirName)
        mkdir(dotGradle)
        writeFile(path.join(dirName, "settings.gradle"), "a", BufferEncoding.utf8)
        writeFile(path.join(dirName, "good.txt"), "a", BufferEncoding.utf8)
        writeFile(path.join(dirName, "bad.txt"), "a", BufferEncoding.utf8)
        writeFile(path.join(dotGradle, "extra.txt"), "a", BufferEncoding.utf8)

        val hash = hashFilesDetailed(
            "$dirName/**/*.gradle",
            "$dirName/**/*.txt",
            "!$dirName/**/.gradle/",
            "!$dirName/**/*bad**",
        )
        val actual = hash.contents.files.entries.joinToString("\n") { (file, details) ->
            "${details.fileSize.formatBytes()} ${details.hash} $file"
        }
        assertEquals(
            """
            1 B 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 ws:///globTest/good.txt
            1 B 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 ws:///globTest/settings.gradle
            """.trimIndent(),
            actual,
        )
    }
}


================================================
FILE: layered-cache/src/jsTest/resources/readme.txt
================================================


================================================
FILE: renovate.json
================================================
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:best-practices"
  ],
  "automerge": true,
  "platformAutomerge": true,
  "labels": ["dependencies"],
  "schedule": ["every 1 weeks on Monday"]
}


================================================
FILE: settings.gradle.kts
================================================
/*
 * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distri
Download .txt
gitextract_dd0zirf7/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── check-action-typing.yml
│       ├── gradle-wrapper-validation.yml
│       └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── NOTICE
├── README.md
├── action-types.yml
├── action.yml
├── build.gradle.kts
├── cache-action-entrypoint/
│   ├── build.gradle.kts
│   ├── src/
│   │   ├── jsMain/
│   │   │   └── kotlin/
│   │   │       ├── main.kt
│   │   │       └── stringArgv.kt
│   │   └── jsTest/
│   │       └── kotlin/
│   │           └── com/
│   │               └── github/
│   │                   └── burrunan/
│   │                       ├── ArgumentsTest.kt
│   │                       └── SplitLinesTest.kt
│   └── webpack.config.d/
│       └── config.js
├── cache-proxy/
│   ├── build.gradle.kts
│   └── src/
│       ├── jsMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── github/
│       │               └── burrunan/
│       │                   └── gradle/
│       │                       └── proxy/
│       │                           └── CacheProxy.kt
│       └── jsTest/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── gradle/
│                               └── proxy/
│                                   └── CacheProxyTest.kt
├── cache-service-mock/
│   ├── build.gradle.kts
│   └── src/
│       └── jsMain/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── gradle/
│                               └── cache/
│                                   ├── CacheService.kt
│                                   ├── CacheStorage.kt
│                                   ├── HttpException.kt
│                                   └── HttpExtensions.kt
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle-launcher/
│   ├── build.gradle.kts
│   └── src/
│       ├── jsMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── github/
│       │               └── burrunan/
│       │                   └── launcher/
│       │                       ├── GradleDistribution.kt
│       │                       ├── GradleInstaller.kt
│       │                       ├── GradleLauncher.kt
│       │                       ├── GradleVersion.kt
│       │                       ├── GradleVersionResponse.kt
│       │                       ├── LaunchParams.kt
│       │                       └── internal/
│       │                           ├── GradleError.kt
│       │                           ├── GradleErrorCollector.kt
│       │                           └── GradleOutErrorCollector.kt
│       └── jsTest/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── launcher/
│                               ├── PropertiesParserTest.kt
│                               ├── RetrieveGradleVersionTest.kt
│                               └── internal/
│                                   ├── GradleErrorCollectorTest.kt
│                                   └── GradleOutCollectorTest.kt
├── gradle.properties
├── gradlew
├── gradlew.bat
├── hashing/
│   ├── build.gradle.kts
│   └── src/
│       └── jsMain/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── hashing/
│                               ├── HashDetails.kt
│                               ├── diff.kt
│                               └── hashFiles.kt
├── layered-cache/
│   ├── build.gradle.kts
│   └── src/
│       ├── jsMain/
│       │   └── kotlin/
│       │       └── com/
│       │           └── github/
│       │               └── burrunan/
│       │                   └── gradle/
│       │                       ├── GradleCacheAction.kt
│       │                       ├── Parameters.kt
│       │                       ├── cache/
│       │                       │   ├── ActionsTriggerExtensions.kt
│       │                       │   ├── Cache.kt
│       │                       │   ├── CompositeCache.kt
│       │                       │   ├── DefaultCache.kt
│       │                       │   ├── GradleGeneratedJarsCache.kt
│       │                       │   ├── LayeredCache.kt
│       │                       │   ├── MetadataFile.kt
│       │                       │   ├── dependenciesCache.kt
│       │                       │   └── localBuildCache.kt
│       │                       └── github/
│       │                           └── StateExtensions.kt
│       └── jsTest/
│           ├── kotlin/
│           │   └── com/
│           │       └── github/
│           │           └── burrunan/
│           │               └── gradle/
│           │                   ├── CacheServerTest.kt
│           │                   └── GlobTest.kt
│           └── resources/
│               └── readme.txt
├── renovate.json
├── settings.gradle.kts
├── test-library/
│   ├── build.gradle.kts
│   └── src/
│       └── jsMain/
│           └── kotlin/
│               └── com/
│                   └── github/
│                       └── burrunan/
│                           └── test/
│                               └── testExtensions.kt
└── wrappers/
    ├── actions-cache/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── actions/
    │                   └── cache/
    │                       ├── CacheExtensions.kt
    │                       ├── RestoreType.kt
    │                       ├── internal/
    │                       │   ├── CacheContract.kt
    │                       │   └── httpclient.kt
    │                       └── types.kt
    ├── actions-toolkit/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── actions/
    │                   ├── core/
    │                   │   ├── ActionFailedException.kt
    │                   │   ├── ActionStage.kt
    │                   │   ├── ActionsEnvironment.kt
    │                   │   ├── LogLevel.kt
    │                   │   ├── LoggingExtensions.kt
    │                   │   └── ext/
    │                   │       ├── Group.kt
    │                   │       └── InputExtensions.kt
    │                   ├── exec/
    │                   │   └── ExecExtensions.kt
    │                   └── glob/
    │                       └── removeFiles.kt
    ├── java-properties/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── javaproperties/
    │                   ├── index.module_java-properties.kt
    │                   └── parseString.kt
    ├── js/
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── com/
    │                   └── github/
    │                       └── burrunan/
    │                           ├── formatBytes.kt
    │                           └── wrappers/
    │                               └── js/
    │                                   └── SuspendExtensions.kt
    ├── nodejs/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── com/
    │                   └── github/
    │                       └── burrunan/
    │                           └── wrappers/
    │                               └── nodejs/
    │                                   ├── FsExtensions.kt
    │                                   └── StreamExtensions.kt
    ├── octokit-request-error/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── octokit/
    │                   └── requesterror/
    │                       ├── index.module_@octokit_request-error.kt
    │                       └── types.module_@octokit_request-error.kt
    ├── octokit-types/
    │   ├── build.gradle.kts
    │   └── src/
    │       └── jsMain/
    │           └── kotlin/
    │               └── octokit/
    │                   └── types/
    │                       ├── AuthInterface.module_@octokit_types.kt
    │                       ├── EndpointDefaults.module_@octokit_types.kt
    │                       ├── EndpointInterface.module_@octokit_types.kt
    │                       ├── EndpointOptions.module_@octokit_types.kt
    │                       ├── Fetch.module_@octokit_types.kt
    │                       ├── GetResponseTypeFromEndpointMethod.module_@octokit_types.kt
    │                       ├── OctokitResponse.module_@octokit_types.kt
    │                       ├── RequestError.module_@octokit_types.kt
    │                       ├── RequestHeaders.module_@octokit_types.kt
    │                       ├── RequestInterface.module_@octokit_types.kt
    │                       ├── RequestOptions.module_@octokit_types.kt
    │                       ├── RequestParameters.module_@octokit_types.kt
    │                       ├── RequestRequestOptions.module_@octokit_types.kt
    │                       ├── ResponseHeaders.module_@octokit_types.kt
    │                       ├── Route.module_@octokit_types.kt
    │                       ├── Signal.module_@octokit_types.kt
    │                       ├── StrategyInterface.module_@octokit_types.kt
    │                       ├── Url.module_@octokit_types.kt
    │                       └── VERSION.module_@octokit_types.kt
    └── octokit-webhooks/
        ├── build.gradle.kts
        └── src/
            └── jsMain/
                └── kotlin/
                    └── octokit/
                        ├── ActionsTrigger.kt
                        └── webhooks/
                            ├── ResponseHeaders.module_@octokit_types.kt
                            ├── event-payloads.EventPayloads.module_@octokit_webhooks.kt
                            ├── get-webhook-payload-type-from-event.module_@octokit_webhooks.kt
                            ├── index.module_@octokit_webhooks.kt
                            └── types.module_@octokit_webhooks.kt
Condensed preview — 124 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (400K chars).
[
  {
    "path": ".editorconfig",
    "chars": 433,
    "preview": "root = true\n\n[*]\ntrim_trailing_whitespace = true\ninsert_final_newline = true\ncharset = utf-8\nindent_style = space\nindent"
  },
  {
    "path": ".gitattributes",
    "chars": 82,
    "preview": "* text=auto\n*.sh text eol=lf\ngradlew text eol=lf\n*.bat text eol=crlf\n*.jar binary\n"
  },
  {
    "path": ".github/workflows/check-action-typing.yml",
    "chars": 465,
    "preview": "name: Check Action Typing\n\non:\n    push:\n      paths:\n        - 'action.yml'\n        - 'action-types.yml'\n    pull_reque"
  },
  {
    "path": ".github/workflows/gradle-wrapper-validation.yml",
    "chars": 254,
    "preview": "name: \"Validate Gradle Wrapper\"\non: [push, pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  validation:\n    name: \""
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 2065,
    "preview": "# The default workflow for GitHub Actions that is used for continuous\n# integration. A configuration file that is used t"
  },
  {
    "path": ".gitignore",
    "chars": 90,
    "preview": "/.idea/\n/.gradle/\n/build/\n/*/build/\n/*/*/build/\n/externals/\n/*/externals/\n/*/*/externals/\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 10529,
    "preview": "## 2024-05-03: v3 🚀 Move to actions/cache@v4 API, resolve \"Cache service responded with 422\"\n* Bump to actions/cache@v4 "
  },
  {
    "path": "LICENSE",
    "chars": 10174,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "NOTICE",
    "chars": 63,
    "preview": "Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n"
  },
  {
    "path": "README.md",
    "chars": 12537,
    "preview": "# Gradle Cache Action\n\n[![CI Status](https://github.com/burrunan/gradle-cache-action/workflows/CI/badge.svg)](https://gi"
  },
  {
    "path": "action-types.yml",
    "chars": 1271,
    "preview": "# See https://github.com/krzema12/github-actions-typing\noutputs:\n  build-scan-url:\n    type: string\ninputs:\n  job-id:\n  "
  },
  {
    "path": "action.yml",
    "chars": 4401,
    "preview": "name: 'Gradle Cache'\ndescription: 'Caches .gradle folder (dependencies, local build cache, ...)'\nauthor: 'Vladimir Sitni"
  },
  {
    "path": "build.gradle.kts",
    "chars": 2819,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-action-entrypoint/build.gradle.kts",
    "chars": 1222,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-action-entrypoint/src/jsMain/kotlin/main.kt",
    "chars": 7140,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-action-entrypoint/src/jsMain/kotlin/stringArgv.kt",
    "chars": 790,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-action-entrypoint/src/jsTest/kotlin/com/github/burrunan/ArgumentsTest.kt",
    "chars": 1785,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-action-entrypoint/src/jsTest/kotlin/com/github/burrunan/SplitLinesTest.kt",
    "chars": 972,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-action-entrypoint/webpack.config.d/config.js",
    "chars": 1621,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-proxy/build.gradle.kts",
    "chars": 1462,
    "preview": "import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest\nimport org.jetbrains.kotlin.gradle.targets.js.testing"
  },
  {
    "path": "cache-proxy/src/jsMain/kotlin/com/github/burrunan/gradle/proxy/CacheProxy.kt",
    "chars": 7651,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-proxy/src/jsTest/kotlin/com/github/burrunan/gradle/proxy/CacheProxyTest.kt",
    "chars": 5415,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-service-mock/build.gradle.kts",
    "chars": 1078,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/CacheService.kt",
    "chars": 6809,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/CacheStorage.kt",
    "chars": 2991,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/HttpException.kt",
    "chars": 1056,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "cache-service-mock/src/jsMain/kotlin/com/github/burrunan/gradle/cache/HttpExtensions.kt",
    "chars": 1438,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 340,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionSha256Sum=bd71102213493060956ec229d946beee5"
  },
  {
    "path": "gradle-launcher/build.gradle.kts",
    "chars": 1392,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleDistribution.kt",
    "chars": 1608,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleInstaller.kt",
    "chars": 6780,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleLauncher.kt",
    "chars": 3098,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleVersion.kt",
    "chars": 1887,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/GradleVersionResponse.kt",
    "chars": 791,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/LaunchParams.kt",
    "chars": 840,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/internal/GradleError.kt",
    "chars": 906,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/internal/GradleErrorCollector.kt",
    "chars": 2537,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsMain/kotlin/com/github/burrunan/launcher/internal/GradleOutErrorCollector.kt",
    "chars": 3701,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/PropertiesParserTest.kt",
    "chars": 1394,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/RetrieveGradleVersionTest.kt",
    "chars": 1310,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/internal/GradleErrorCollectorTest.kt",
    "chars": 3197,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle-launcher/src/jsTest/kotlin/com/github/burrunan/launcher/internal/GradleOutCollectorTest.kt",
    "chars": 5257,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "gradle.properties",
    "chars": 736,
    "preview": "#\n# Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (t"
  },
  {
    "path": "gradlew",
    "chars": 8710,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "gradlew.bat",
    "chars": 2937,
    "preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
  },
  {
    "path": "hashing/build.gradle.kts",
    "chars": 1035,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "hashing/src/jsMain/kotlin/com/github/burrunan/hashing/HashDetails.kt",
    "chars": 4101,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "hashing/src/jsMain/kotlin/com/github/burrunan/hashing/diff.kt",
    "chars": 2931,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "hashing/src/jsMain/kotlin/com/github/burrunan/hashing/hashFiles.kt",
    "chars": 2746,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/build.gradle.kts",
    "chars": 1506,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/GradleCacheAction.kt",
    "chars": 3072,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/Parameters.kt",
    "chars": 1063,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/ActionsTriggerExtensions.kt",
    "chars": 1430,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/Cache.kt",
    "chars": 807,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/CompositeCache.kt",
    "chars": 1842,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/DefaultCache.kt",
    "chars": 5916,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/GradleGeneratedJarsCache.kt",
    "chars": 1136,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/LayeredCache.kt",
    "chars": 10251,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/MetadataFile.kt",
    "chars": 2805,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/dependenciesCache.kt",
    "chars": 3334,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/cache/localBuildCache.kt",
    "chars": 1870,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsMain/kotlin/com/github/burrunan/gradle/github/StateExtensions.kt",
    "chars": 2616,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsTest/kotlin/com/github/burrunan/gradle/CacheServerTest.kt",
    "chars": 3719,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsTest/kotlin/com/github/burrunan/gradle/GlobTest.kt",
    "chars": 2121,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "layered-cache/src/jsTest/resources/readme.txt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "renovate.json",
    "chars": 240,
    "preview": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:best-practices\"\n  ],\n  \"a"
  },
  {
    "path": "settings.gradle.kts",
    "chars": 1353,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "test-library/build.gradle.kts",
    "chars": 906,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "test-library/src/jsMain/kotlin/com/github/burrunan/test/testExtensions.kt",
    "chars": 888,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-cache/build.gradle.kts",
    "chars": 869,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-cache/src/jsMain/kotlin/actions/cache/CacheExtensions.kt",
    "chars": 2585,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-cache/src/jsMain/kotlin/actions/cache/RestoreType.kt",
    "chars": 960,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-cache/src/jsMain/kotlin/actions/cache/internal/CacheContract.kt",
    "chars": 1787,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-cache/src/jsMain/kotlin/actions/cache/internal/httpclient.kt",
    "chars": 1574,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-cache/src/jsMain/kotlin/actions/cache/types.kt",
    "chars": 757,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/build.gradle.kts",
    "chars": 1075,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/core/ActionFailedException.kt",
    "chars": 766,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/core/ActionStage.kt",
    "chars": 697,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/core/ActionsEnvironment.kt",
    "chars": 1788,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/core/LogLevel.kt",
    "chars": 77,
    "preview": "package actions.core\n\nenum class LogLevel {\n    DEBUG,\n    INFO,\n    NONE,\n}\n"
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/core/LoggingExtensions.kt",
    "chars": 947,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/core/ext/Group.kt",
    "chars": 868,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/core/ext/InputExtensions.kt",
    "chars": 474,
    "preview": "package actions.core.ext\n\nimport actions.core.InputOptions\n\nfun getInput(name: String, required: Boolean = false): Strin"
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/exec/ExecExtensions.kt",
    "chars": 1778,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/actions-toolkit/src/jsMain/kotlin/actions/glob/removeFiles.kt",
    "chars": 1099,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/java-properties/build.gradle.kts",
    "chars": 783,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/java-properties/src/jsMain/kotlin/javaproperties/index.module_java-properties.kt",
    "chars": 1216,
    "preview": "@file:JsModule(\"java-properties\")\n@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MI"
  },
  {
    "path": "wrappers/java-properties/src/jsMain/kotlin/javaproperties/parseString.kt",
    "chars": 827,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/js/src/jsMain/kotlin/com/github/burrunan/formatBytes.kt",
    "chars": 946,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/js/src/jsMain/kotlin/com/github/burrunan/wrappers/js/SuspendExtensions.kt",
    "chars": 1454,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/nodejs/build.gradle.kts",
    "chars": 837,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/nodejs/src/jsMain/kotlin/com/github/burrunan/wrappers/nodejs/FsExtensions.kt",
    "chars": 1210,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/nodejs/src/jsMain/kotlin/com/github/burrunan/wrappers/nodejs/StreamExtensions.kt",
    "chars": 1233,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/octokit-request-error/build.gradle.kts",
    "chars": 782,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/octokit-request-error/src/jsMain/kotlin/octokit/requesterror/index.module_@octokit_request-error.kt",
    "chars": 822,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/octokit-request-error/src/jsMain/kotlin/octokit/requesterror/types.module_@octokit_request-error.kt",
    "chars": 974,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/octokit-types/build.gradle.kts",
    "chars": 776,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/AuthInterface.module_@octokit_types.kt",
    "chars": 788,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/EndpointDefaults.module_@octokit_types.kt",
    "chars": 583,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/EndpointInterface.module_@octokit_types.kt",
    "chars": 1898,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/EndpointOptions.module_@octokit_types.kt",
    "chars": 283,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/Fetch.module_@octokit_types.kt",
    "chars": 177,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/GetResponseTypeFromEndpointMethod.module_@octokit_types.kt",
    "chars": 487,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/OctokitResponse.module_@octokit_types.kt",
    "chars": 286,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/RequestError.module_@octokit_types.kt",
    "chars": 422,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/RequestHeaders.module_@octokit_types.kt",
    "chars": 494,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/RequestInterface.module_@octokit_types.kt",
    "chars": 996,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/RequestOptions.module_@octokit_types.kt",
    "chars": 383,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/RequestParameters.module_@octokit_types.kt",
    "chars": 557,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/RequestRequestOptions.module_@octokit_types.kt",
    "chars": 438,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/ResponseHeaders.module_@octokit_types.kt",
    "chars": 602,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/Route.module_@octokit_types.kt",
    "chars": 180,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/Signal.module_@octokit_types.kt",
    "chars": 178,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/StrategyInterface.module_@octokit_types.kt",
    "chars": 410,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/Url.module_@octokit_types.kt",
    "chars": 178,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-types/src/jsMain/kotlin/octokit/types/VERSION.module_@octokit_types.kt",
    "chars": 181,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-webhooks/build.gradle.kts",
    "chars": 910,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/octokit-webhooks/src/jsMain/kotlin/octokit/ActionsTrigger.kt",
    "chars": 2137,
    "preview": "/*\n * Copyright 2020 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>\n *\n * Licensed under the Apache License, Version 2."
  },
  {
    "path": "wrappers/octokit-webhooks/src/jsMain/kotlin/octokit/webhooks/ResponseHeaders.module_@octokit_types.kt",
    "chars": 580,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-webhooks/src/jsMain/kotlin/octokit/webhooks/event-payloads.EventPayloads.module_@octokit_webhooks.kt",
    "chars": 137040,
    "preview": "@file:JsModule(\"@octokit/webhooks\")\n@file:JsQualifier(\"EventPayloads\")\n@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVER"
  },
  {
    "path": "wrappers/octokit-webhooks/src/jsMain/kotlin/octokit/webhooks/get-webhook-payload-type-from-event.module_@octokit_webhooks.kt",
    "chars": 259,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-webhooks/src/jsMain/kotlin/octokit/webhooks/index.module_@octokit_webhooks.kt",
    "chars": 277,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  },
  {
    "path": "wrappers/octokit-webhooks/src/jsMain/kotlin/octokit/webhooks/types.module_@octokit_webhooks.kt",
    "chars": 1750,
    "preview": "@file:Suppress(\"INTERFACE_WITH_SUPERCLASS\", \"OVERRIDING_FINAL_MEMBER\", \"RETURN_TYPE_MISMATCH_ON_OVERRIDE\", \"CONFLICTING_"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the burrunan/gradle-cache-action GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 124 files (365.4 KB), approximately 92.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!