├── .github └── workflows │ ├── ci.yml │ └── clean.yml ├── .gitignore ├── .scala-steward.conf ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── build.sbt ├── libraries.sbt ├── project ├── build.properties ├── libraries.sbt ├── plugins.sbt ├── project │ ├── build.properties │ ├── build.sbt │ ├── libraries.sbt │ └── src │ │ └── main └── src │ └── main └── src ├── main └── scala │ └── sbtghactions │ ├── GenerativeKeys.scala │ ├── GenerativePlugin.scala │ ├── GitHubActionsKeys.scala │ ├── GitHubActionsPlugin.scala │ ├── JavaSpec.scala │ ├── JobContainer.scala │ ├── JobEnvironment.scala │ ├── PREventType.scala │ ├── Paths.scala │ ├── PermissionScope.scala │ ├── Ref.scala │ ├── RefPredicate.scala │ ├── UseRef.scala │ ├── WorkflowJob.scala │ ├── WorkflowStep.scala │ ├── matrix.scala │ └── windows │ └── PagefileFix.scala ├── sbt-test └── sbtghactions │ ├── check-and-regenerate │ ├── build.sbt │ ├── expected-ci.yml │ ├── expected-clean.yml │ ├── project │ │ └── plugins.sbt │ └── test │ ├── githubworkflowoses-clean-publish │ ├── .github │ │ └── workflows │ │ │ ├── ci.yml │ │ │ └── clean.yml │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ └── test │ ├── no-clean │ ├── .github │ │ └── workflows │ │ │ └── ci.yml │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ └── test │ ├── non-existent-target │ ├── .github │ │ └── workflows │ │ │ ├── ci.yml │ │ │ └── clean.yml │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ └── test │ ├── sbt-native-thin-client │ ├── .github │ │ └── workflows │ │ │ ├── ci.yml │ │ │ └── clean.yml │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ └── test │ └── suppressed-scala-version │ ├── build.sbt │ ├── expected-ci.yml │ ├── expected-clean.yml │ ├── project │ └── plugins.sbt │ └── test └── test └── scala └── sbtghactions └── GenerativePluginSpec.scala /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | tags: [v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | jobs: 21 | build: 22 | name: Build and Test 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | scala: [2.12.20] 27 | java: [zulu@8, graal_graalvm@17, corretto@17] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - name: Ignore line ending differences in git 31 | if: contains(runner.os, 'windows') 32 | shell: bash 33 | run: git config --global core.autocrlf false 34 | 35 | - name: Configure pagefile for Windows 36 | if: contains(runner.os, 'windows') 37 | uses: al-cheb/configure-pagefile-action@v1.4 38 | with: 39 | minimum-size: 2GB 40 | maximum-size: 8GB 41 | disk-root: 'C:' 42 | 43 | - name: Checkout current branch (full) 44 | uses: actions/checkout@v4 45 | with: 46 | fetch-depth: 0 47 | 48 | - name: Setup Java (zulu@8) 49 | if: matrix.java == 'zulu@8' 50 | uses: actions/setup-java@v4 51 | with: 52 | distribution: zulu 53 | java-version: 8 54 | cache: sbt 55 | 56 | - name: Setup GraalVM (graal_graalvm@17) 57 | if: matrix.java == 'graal_graalvm@17' 58 | uses: graalvm/setup-graalvm@v1 59 | with: 60 | java-version: 17 61 | distribution: graalvm 62 | components: native-image 63 | github-token: ${{ secrets.GITHUB_TOKEN }} 64 | cache: sbt 65 | 66 | - name: Setup Java (corretto@17) 67 | if: matrix.java == 'corretto@17' 68 | uses: actions/setup-java@v4 69 | with: 70 | distribution: corretto 71 | java-version: 17 72 | cache: sbt 73 | 74 | - name: Setup sbt 75 | uses: sbt/setup-sbt@v1 76 | 77 | - name: Check that workflows are up to date 78 | shell: bash 79 | run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck 80 | 81 | - shell: bash 82 | run: sbt '++ ${{ matrix.scala }}' '+ test' scripted 83 | 84 | - name: Clean up Ivy Local repo 85 | shell: bash 86 | run: rm -rf "$HOME/.ivy2/local" 87 | 88 | - name: Compress target directories 89 | shell: bash 90 | run: tar cf targets.tar target project/target 91 | 92 | - name: Upload target directories 93 | uses: actions/upload-artifact@v4 94 | with: 95 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 96 | path: targets.tar 97 | 98 | publish: 99 | name: Publish Artifacts 100 | needs: [build] 101 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 102 | strategy: 103 | matrix: 104 | os: [ubuntu-latest] 105 | scala: [2.12.20] 106 | java: [zulu@8] 107 | runs-on: ${{ matrix.os }} 108 | steps: 109 | - name: Ignore line ending differences in git 110 | if: contains(runner.os, 'windows') 111 | run: git config --global core.autocrlf false 112 | 113 | - name: Configure pagefile for Windows 114 | if: contains(runner.os, 'windows') 115 | uses: al-cheb/configure-pagefile-action@v1.4 116 | with: 117 | minimum-size: 2GB 118 | maximum-size: 8GB 119 | disk-root: 'C:' 120 | 121 | - name: Checkout current branch (full) 122 | uses: actions/checkout@v4 123 | with: 124 | fetch-depth: 0 125 | 126 | - name: Setup Java (zulu@8) 127 | if: matrix.java == 'zulu@8' 128 | uses: actions/setup-java@v4 129 | with: 130 | distribution: zulu 131 | java-version: 8 132 | cache: sbt 133 | 134 | - name: Setup GraalVM (graal_graalvm@17) 135 | if: matrix.java == 'graal_graalvm@17' 136 | uses: graalvm/setup-graalvm@v1 137 | with: 138 | java-version: 17 139 | distribution: graalvm 140 | components: native-image 141 | github-token: ${{ secrets.GITHUB_TOKEN }} 142 | cache: sbt 143 | 144 | - name: Setup Java (corretto@17) 145 | if: matrix.java == 'corretto@17' 146 | uses: actions/setup-java@v4 147 | with: 148 | distribution: corretto 149 | java-version: 17 150 | cache: sbt 151 | 152 | - name: Setup sbt 153 | uses: sbt/setup-sbt@v1 154 | 155 | - name: Download target directories (2.12.20) 156 | uses: actions/download-artifact@v4 157 | with: 158 | name: target-${{ matrix.os }}-2.12.20-${{ matrix.java }} 159 | 160 | - name: Inflate target directories (2.12.20) 161 | run: | 162 | tar xf targets.tar 163 | rm targets.tar 164 | 165 | - name: Publish project 166 | env: 167 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 168 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 169 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 170 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 171 | run: sbt ci-release 172 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | shell: bash {0} 21 | run: | 22 | # Customize those three lines with your repository and credentials: 23 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 24 | 25 | # A shortcut to call GitHub API. 26 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 27 | 28 | # A temporary file which receives HTTP response headers. 29 | TMPFILE=$(mktemp) 30 | 31 | # An associative array, key: artifact name, value: number of artifacts of that name. 32 | declare -A ARTCOUNT 33 | 34 | # Process all artifacts on this repository, loop on returned "pages". 35 | URL=$REPO/actions/artifacts 36 | while [[ -n "$URL" ]]; do 37 | 38 | # Get current page, get response headers in a temporary file. 39 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 40 | 41 | # Get URL of next page. Will be empty if we are at the last page. 42 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 43 | rm -f $TMPFILE 44 | 45 | # Number of artifacts on this page: 46 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 47 | 48 | # Loop on all artifacts on this page. 49 | for ((i=0; $i < $COUNT; i++)); do 50 | 51 | # Get name of artifact and count instances of this name. 52 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 53 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 54 | 55 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 56 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 57 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 58 | ghapi -X DELETE $REPO/actions/artifacts/$id 59 | done 60 | done 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### SBT template 2 | # Simple Build Tool 3 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 4 | 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | .history 12 | .cache 13 | .lib/ 14 | 15 | ### Scala template 16 | *.class 17 | *.log 18 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.pin = [ 2 | { groupId = "org.scala-lang", artifactId = "scala-library", version = "2.12." } 3 | ] 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to sbt-github-actions 2 | 3 | ## Windows 4 | 5 | Currently, the project uses symbolic links which requires special handling when working on Windows. 6 | Firstly you need to make sure that you have [mklink](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/mklink) 7 | permissions as a user, the easiest way to do this is if you happen to be running Windows 10/Windows 11 8 | is to enable [Developer Mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging?source=recommendations). 9 | After this is done then you can enable `symlinks` globally by doing 10 | 11 | ```shell 12 | git config --global core.symlinks true 13 | ``` 14 | 15 | Alternately if you don't want to enable `symlinks` globally you can just selectively enable it when checking 16 | out this repository, i.e. 17 | 18 | ```shell 19 | git clone -c core.symlinks=true git@github.com:sbt/sbt-github-actions.git 20 | ``` 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sbt-github-actions 2 | 3 | sbt-github-actions is an sbt plugin for assisting in building sbt projects using [GitHub Actions](https://github.com/features/actions). 4 | 5 | ## Usage 6 | 7 | Add the following to your `plugins.sbt`: 8 | 9 | ```sbt 10 | addSbtPlugin("com.github.sbt" % "sbt-github-actions" % ) 11 | ``` 12 | 13 | To use the generative functionality, run `sbt githubWorkflowGenerate` and *commit the results*. If your sbt build is ever changed such that the generated workflow is no longer in sync, the workflow run in GitHub Actions will begin failing and you will need to re-run this task (and commit the results). 14 | 15 | ## General Plugin 16 | 17 | The `GitHubActionsPlugin` provides general functionality, giving builds the ability to introspect on their host workflow and whether or not they are running in GitHub Actions. This latter functionality, exposed by the `githubIsWorkflowBuild` global setting, is the most commonly used functionality of this plugin. If you need behavior within your build which is conditional on whether or not the build is running in CI, this is the setting you should branch on. 18 | 19 | `githubWorkflowName` and `githubWorkflowDefinition` are designed to allow introspection on the *exact* definition of the workflow which is running the current build, if any. This kind of introspection is not common, but seems like it could be useful. 20 | 21 | ## Generative Plugin 22 | 23 | sbt-github-actions provides a mechanism for *generating* GitHub Actions workflows from the sbt build definition. With sbt-github-actions, the `scala:` entry in the job `matrix:` is populated from the `ThisBuild / crossScalaVersions` key in your **build.sbt**. 24 | 25 | The `GenerativePlugin` is designed to make it easier to maintain GitHub Actions builds for sbt projects by generating **ci.yml** and **clean.yml** workflow definition files, and then forcibly failing the build if these files ever fall out of step with the build itself. The **ci.yml** workflow, by default, contains both `build` and `publish` jobs, though you will likely need to add extra steps to the `githubWorkflowPublishPreamble` and/or `githubWorkflowEnv` (e.g. decrypting and importing a GPG signing key) in order for publication to *actually* work. 26 | 27 | If a `publish` job is not desired, simply set `githubWorkflowPublishTargetBranches` to `Seq()`. By default, `publish` is restricted to run on `main`, and additional restrictions may be configured within the build. 28 | 29 | sbt and Coursier caching are all handled by the generated **ci.yml** by default, as well as standard things like Git checkout, Scala setup (using [excellent `setup-java` action](https://github.com/actions/setup-java)), and more. The matrix for the `build` job will be generated from `crossScalaVersions` and has additional support for multiple JVMs and OSes. Additionally, compiled artifacts are properly uploaded so that jobs which are dependent on `build` can avoid redundant work (most notably, `publish`). Thus, publication is guaranteed to be based on binary files that were generated *and* tested by the `build` job, rather than re-generated by `publish`. (**NB**: due to what appear to be issues in Zinc, this isn't *quite* working yet; expect it to be fixed in a coming release of sbt-github-actions) 30 | 31 | **clean.yml** is generated based on a static description because it *should* just be the default in all GitHub Actions projects. This is basically a hack to work around the fact that artifacts produced by GitHub Actions workflows count against personal and organization storage limits, but those artifacts also are retained *indefinitely* up until 2 GB. This is entirely unnecessary and egregious, since artifacts are transient and only useful for passing state between jobs within the same workflow. To make matters more complicated, artifacts from a given workflow are invisible to the GitHub API until that workflow is finished, which is why **clean.yml** has to be a *separate* workflow rather than part of **ci.yml**. It runs on every push to the repository. 32 | 33 | This plugin is quite prescriptive in that it forcibly manages the contents of the **ci.yml** and **clean.yml** files. By default, **ci.yml** will contain a step which *verifies* that its contents (and the contents of **clean.yml**) correspond precisely to the most up-to-date generated version of themselves. If this is not the case, then the build is failed. However, there is no restriction in adding *other* workflows not named **ci.yml** or **clean.yml**. These will be ignored entirely by the plugin. 34 | 35 | ### JDK settings 36 | 37 | We recommend you set the following setting in `build.sbt`: 38 | 39 | ```scala 40 | // sbt-github-actions defaults to using JDK 8 for testing and publishing. 41 | // The following adds JDK 17 for testing. 42 | ThisBuild / githubWorkflowJavaVersions += JavaSpec.temurin("17") 43 | ``` 44 | 45 | 46 | ### Integration with sbt-ci-release 47 | 48 | Integrating with [sbt-ci-release](https://github.com/olafurpg/sbt-ci-release) is a relatively straightforward process, and the plugins are quite complementary. First, follow all of the setup instructions in sbt-ci-release's readme. Once this is complete, add the following to your **build.sbt**: 49 | 50 | ```scala 51 | ThisBuild / githubWorkflowTargetTags ++= Seq("v*") 52 | ThisBuild / githubWorkflowPublishTargetBranches := 53 | Seq(RefPredicate.StartsWith(Ref.Tag("v"))) 54 | 55 | ThisBuild / githubWorkflowPublish := Seq( 56 | WorkflowStep.Sbt( 57 | commands = List("ci-release"), 58 | name = Some("Publish project"), 59 | ) 60 | ) 61 | ``` 62 | 63 | This is assuming that you *only* wish to publish tags. If you also wish to publish snapshots upon successful main builds, use the following `githubWorkflowPublishTargetBranches` declaration: 64 | 65 | ```scala 66 | ThisBuild / githubWorkflowPublishTargetBranches := 67 | Seq( 68 | RefPredicate.StartsWith(Ref.Tag("v")), 69 | RefPredicate.Equals(Ref.Branch("main")) 70 | ) 71 | ``` 72 | 73 | Note the use of `+=` rather than `:=`. 74 | 75 | ```scala 76 | ThisBuild / githubWorkflowPublish := Seq( 77 | WorkflowStep.Sbt( 78 | commands = List("ci-release"), 79 | name = Some("Publish project"), 80 | env = Map( 81 | "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", 82 | "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", 83 | "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", 84 | "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}" 85 | ) 86 | ) 87 | ) 88 | ``` 89 | 90 | ## Tasks 91 | 92 | ### Generative 93 | 94 | - `githubWorkflowGenerate` – Generates (and overwrites if extant) **ci.yml** and **clean.yml** workflows according to configuration within sbt. The **clean.yml** workflow is something that GitHub Actions should just do by default: it removes old build artifacts to prevent them from running up your storage usage (it has no effect on currently running builds). This workflow is unconfigurable and is simply drawn from the static contents of the **clean.yml** resource file within this repository. 95 | - `githubWorkflowCheck` – Checks to see if the **ci.yml** and **clean.yml** files are equivalent to what would be generated and errors if otherwise. This task is run from within the generated **ci.yml** to ensure that the build and the workflow are kept in sync. As a general rule, any time you change the workflow configuration within sbt, you should regenerate the **ci.yml** and commit the results, but inevitably people forget. This check fails the build if that happens. Note that if you *need* to manually fiddle with the **ci.yml** contents, for whatever reason, you will need to remove the call to this check from within the workflow, otherwise your build will simply fail. 96 | 97 | ## Settings 98 | 99 | ### General 100 | 101 | - `githubWorkflowDir` : `File` - Where to place the workflow directory which contains the generated ci.yml and clean.yml files. (default: `baseDirectory.value / ".github"`) 102 | - `githubIsWorkflowBuild` : `Boolean` – Indicates whether or not the build is currently running within a GitHub Actions Workflow 103 | - `githubWorkflowName` : `String` – The name of the currently-running workflow. Will be undefined if not running in GitHub Actions. 104 | - `githubWorkflowDefinition` : `Map[String, Any]` – The raw (parsed) contents of the workflow YAML definition. Will be undefined if not running in GitHub Actions, or if (for some reason) the workflow could not be identified. Workflows are located by taking the `githubWorkflowName` and finding the YAML definition which has the corresponding `name:` key/value pair. 105 | 106 | ### Generative 107 | 108 | Any and all settings which affect the behavior of the generative plugin should be set in the `ThisBuild` scope (for example, `ThisBuild / crossScalaVersions :=` rather than just `crossScalaVersions := `). This is important because GitHub Actions workflows are global across the entire build, regardless of how individual projects are configured. A corollary of this is that it is not possible (yet) to have specific subprojects which build with different Scala versions, Java versions, or OSes. This is theoretically possible but it's very complicated. For now, I'm going to be lazy and wait for someone to say "pretty please" before implementing it. 109 | 110 | #### General 111 | 112 | - `githubWorkflowGeneratedCI` : `Seq[WorkflowJob]` — Contains a description of the **ci.yml** jobs that will drive the generation if used. This setting can be overridden to customize the jobs (e.g. by adding additional jobs to the workflow). 113 | - `githubWorkflowGeneratedUploadSteps` : `Seq[WorkflowStep]` – Contains a list of steps which are used to upload generated intermediate artifacts from the `build` job. This is mostly for reference and introspection purposes; one would not be expected to *change* this setting. 114 | - `githubWorkflowGeneratedDownloadSteps` : `Seq[WorkflowStep]` – Contains a list of steps which are used to download generated intermediate artifacts from the `build` job. This is mostly for reference and introspection purposes; one would not be expected to *change* this setting. This setting is particularly useful in conjunction with `githubWorkflowAddedJobs`: if you're adding a job which needs access to intermediate artifacts, you should make sure these steps are part of the process. 115 | - `githubWorkflowGeneratedCacheSteps` : `Seq[WorkflowStep]` – Contains a list of steps which are used to set up caching for ivy, sbt, and coursier artifacts (respecting changes to files which contain versioning information). This is mostly for reference and introspection purposes; one would not be expected to *change* this setting. This setting is particularly useful in conjunction with `githubWorkflowAddedJobs`: if you're adding a job which needs to use sbt, you should probably ensure that these steps are part of the job. 116 | - `githubWorkflowSbtCommand` : `String` – Any use of sbt within the generated workflow will compile to an invocation of this bash command. This defaults to `"sbt"`, but can be overridden to anything that is valid in bash syntax (e.g. `"csbt"`, or `"$SBT"`). 117 | - `githubWorkflowUseSbtThinClient` : `Boolean` – Controls whether or not the `--client` option will be added to `sbt` command invocations, accelerating build times (default: `true` for sbt ≥ 1.4, `false` otherwise) 118 | - `githubWorkflowIncludeClean` : `Boolean` – Controls whether to include the clean.yml file (default: `true`) 119 | - `githubWorkflowDependencyPatterns` : `Seq[String]` – A list of file globs which dictate where dependency information is stored. This is conventionally just `**/*.sbt` and `project/build.properties`. If you store dependency information in some *other* file (for example, `project/Versions.scala`), then you should add a glob which matches that file in this setting. This is used for determining the appropriate cache keys for the Ivy and Coursier caches. 120 | - `githubWorkflowTargetBranches` : `Seq[String]` – A list of globs which will match branches and tags for `push` and `pull-request` event types to trigger the **ci.yml** workflow. Defaults to `[*]`. 121 | - `githubWorkflowTargetTags` : `Seq[String]` – A list of globs which will match tags and tags for `push` event types to trigger the **ci.yml** workflow. Defaults to `[]`. 122 | - `githubWorkflowTargetPaths` : `Paths` – Paths which will match modified files for `push` and `pull_request` event types to trigger the **ci.yml** workflow. May be `Paths.None`, `Paths.Include(patterns)`, or `Paths.Ignore(patterns)`. `Paths.Include` may include negative patterns. Defaults to `Paths.None`. 123 | - `githubWorkflowPREventTypes` : `Seq[PREventType]` – A list of event types which will be used to determine which Pull Request events trigger the **ci.yml** workflow. This follows GitHub's defaults: `[opened, synchronize, reopened]`. 124 | - `githubWorkflowArtifactUpload` : `Boolean` – Controls whether or not to upload target directories in the event that multiple jobs are running sequentially. Can be set on a per-project basis. Defaults to `true`. 125 | - `githubWorkflowJobSetup` : `Seq[WorkflowStep]` – The automatically-generated checkout, setup, and cache steps which are common to all jobs which touch the build (default: autogenerated) 126 | - `githubWorkflowEnv` : `Map[String, String]` – An environment which is global to the entire **ci.yml** workflow. Defaults to `Map("GITHUB_TOKEN" -> "${{ secrets.GITHUB_TOKEN }}")` since it's so commonly needed. 127 | - `githubWorkflowAddedJobs` : `Seq[WorkflowJob]` – A convenience mechanism for adding extra custom jobs to the **ci.yml** workflow (though you can also do this by modifying `githubWorkflowGeneratedCI`). Defaults to empty. 128 | 129 | #### `build` Job 130 | 131 | - `githubWorkflowBuildMatrixFailFast` : `Option[Boolean]` – Whether or not to enable the `fail-fast` strategy for the `build` job. By default, no strategy option is written to the build configuration (`None`), which means that the `fail-fast` strategy *is* used. 132 | - `githubWorkflowBuildMatrixAdditions` : `Map[String, List[String]]` – Contains a map of additional `matrix:` dimensions which will be added to the `build` job (on top of the auto-generated ones for Scala/Java/JVM version). As an example, this can be used to manually achieve additional matrix expansion for ScalaJS compilation. Matrix variables can be referenced in the conventional way within steps by using the `${{ matrix.keynamehere }}` syntax. Defaults to empty. 133 | - `githubWorkflowBuildMatrixInclusions` : `Seq[MatrixInclude]` – A list of [matrix *inclusions*](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-including-additional-values-into-combinations). This is useful for when you have a specific matrix job which needs to do extra work, or wish to add an individual matrix job to the configuration set. The matching keys and values are verified against the known matrix configuration. Defaults to empty. 134 | - `githubWorkflowBuildMatrixExclusions` : `Seq[MatrixExclude]` – A list of [matrix *exclusions*](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-excluding-configurations-from-a-matrix). This is useful for when there is a matrix expansion (or set of expansions) which you wish to filter out of the set. Note that exclusions are applied *before* inclusions, allowing you to subtract jobs before re-adding them. Also – and the documentation isn't clear on this point – it is possible that the matching must cover the full set of matrix keys and cannot contain partial values. Defaults to empty. 135 | - `githubWorkflowBuildPreamble` : `Seq[WorkflowStep]` – Contains a list of steps which will be inserted into the `build` job in the **ci.yml** workflow *after* setup but *before* the `sbt test` invocation. Defaults to empty. 136 | - `githubWorkflowBuildPostamble` : `Seq[WorkflowStep]` – Similar to the `Preamble` variant, this contains a list of steps which will be added to the `build` job *after* the `sbt test` invocation but before cleanup. Defaults to empty. 137 | - `githubWorkflowBuildSbtStepPreamble` : `Seq[String]` - See below. 138 | - `githubWorkflowBuild` : `Seq[WorkflowStep]` – The steps which invoke sbt (or whatever else you want) to build and test your project. This defaults to just `[sbt test]`, but can be overridden to anything. For example, sbt plugin projects probably want to redefine this to be `Seq(WorkflowStep.Sbt(List("test", "scripted")))`, which would run the `test` and `scripted` sbt tasks, in order. Note that all uses of `WorkflowStep.Sbt` are compiled using the configured `githubWorkflowSbtCommand` invocation, and prepended with `githubWorkflowBuildSbtStepPreamble` (default: `[++{matrix.scala}]`). 139 | - `githubWorkflowJavaVersions` : `Seq[JavaSpec]` – A list of Java versions to be used for the build job. The publish job will use the *first* of these versions. Defaults to `JavaSpec.temurin("8")`). 140 | - `githubWorkflowScalaVersions` : `Seq[String]` – A list of Scala versions which will be used to `build` your project. Defaults to `crossScalaVersions` in `build`, and simply `scalaVersion` in `publish`. 141 | - `githubWorkflowOSes` : `Seq[String]` – A list of operating systems, which will be ultimately passed to [the `runs-on:` directive](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on), on which to `build` your project. Defaults to `ubuntu-latest`. Note that, regardless of the value of this setting, only `ubuntu-latest` will be used for the `publish` job. The first value of this setting will be used for the clean job. 142 | - `githubWorkflowBuildRunsOnExtraLabels` : `Seq[String]` - A list of additional runs-on labels, which will be combined with the matrix.os from `githubWorkflowOSes` above allowing for singling out more specific runners. 143 | - `githubWorkflowBuildTimeout` : `Option[FiniteDuration]` - [The maximum duration](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) to let the build job run before GitHub automatically cancels it. Defaults to `None`. 144 | 145 | #### `publish` Job 146 | 147 | - `githubWorkflowPublishPreamble` : `Seq[WorkflowStep]` – Similar to `githubWorkflowBuildPreamble`, this contains a series of steps which will be inserted into the `publish` job *after* setup but *before* the publication step. Defaults to empty. 148 | - `githubWorkflowPublishPostamble` : `Seq[WorkflowStep]` – Similar to the `Preamble` variant, this contains a series of steps which will be inserted into the `publish` job after publication has completed, but before cleanup. Defaults to empty. 149 | - `githubWorkflowPublishSbtStepPreamble` : `Seq[String]` - Defaults to `Nil`. This opts out of `++{matrix.scala}` preamble during publishing since publishing step typically includes cross building. 150 | - `githubWorkflowPublish` : `Seq[WorkflowStep]` – The steps which will be invoked to publish your project. This defaults to `[sbt +publish]`. 151 | - `githubWorkflowPublishTargetBranches` : `Seq[RefPredicate]` – A list of branch predicates which will be applied to determine whether the `publish` job will run. Defaults to just `== main`. The supports all of the predicate types currently [allowed by GitHub Actions](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#functions). This exists because, while you usually want to run the `build` job on *every* branch, `publish` is obviously much more limited in applicability. If this list is empty, then the `publish` job will be omitted entirely from the workflow. 152 | - `githubWorkflowPublishCond` : `Option[String]` – This is an optional added conditional check on the publish branch, which must be defined using [GitHub Actions expression syntax](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#about-contexts-and-expressions), which will be conjoined to determine the `if:` predicate on the `publish` job. Defaults to `None`. 153 | - `githubWorkflowPublishTimeout` : `Option[FiniteDuration]` - [The maximum duration](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) to let the publish job run before GitHub automatically cancels it. Defaults to `None`. 154 | 155 | #### Windows related 156 | 157 | - `githubWorkflowWindowsPagefileFix` : `Option[windows.PagefileFix]` - Due to the fact that Windows is more strict in how it treats pagefile size compared to *unix systems, certain windows related workflows (typically scala-native) can run out of memory without this fix. Defaults to `Some(windows.PagefileFix(2GB,8GB))`. 158 | 159 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | name := "sbt-github-actions" 18 | 19 | lazy val scala212 = "2.12.20" 20 | ThisBuild / organization := "com.github.sbt" 21 | ThisBuild / crossScalaVersions := Seq(scala212) 22 | ThisBuild / scalaVersion := scala212 23 | 24 | ThisBuild / githubWorkflowOSes := Seq("ubuntu-latest", "macos-latest", "windows-latest") 25 | ThisBuild / githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("+ test", "scripted"))) 26 | ThisBuild / githubWorkflowBuildPostamble += WorkflowStep.Run( 27 | commands = List("""rm -rf "$HOME/.ivy2/local""""), 28 | name = Some("Clean up Ivy Local repo") 29 | ) 30 | ThisBuild / githubWorkflowJavaVersions ++= Seq( 31 | JavaSpec.graalvm(Graalvm.Distribution("graalvm"), "17"), 32 | JavaSpec.corretto("17") 33 | ) 34 | 35 | ThisBuild / githubWorkflowTargetTags ++= Seq("v*") 36 | ThisBuild / githubWorkflowPublishTargetBranches := 37 | Seq( 38 | RefPredicate.StartsWith(Ref.Tag("v")), 39 | RefPredicate.Equals(Ref.Branch("main")) 40 | ) 41 | ThisBuild / githubWorkflowPublish := Seq( 42 | WorkflowStep.Sbt( 43 | commands = List("ci-release"), 44 | name = Some("Publish project"), 45 | env = Map( 46 | "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", 47 | "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", 48 | "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", 49 | "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}" 50 | ) 51 | ) 52 | ) 53 | 54 | pluginCrossBuild / sbtVersion := { 55 | scalaBinaryVersion.value match { 56 | case "2.12" => 57 | "1.5.5" 58 | case _ => 59 | "2.0.0-M3" 60 | } 61 | } 62 | crossScalaVersions += "3.6.2" 63 | 64 | publishMavenStyle := true 65 | 66 | scalacOptions += 67 | "-Xlint:_,-missing-interpolator" 68 | 69 | libraryDependencies += "org.specs2" %% "specs2-core" % "4.20.8" % Test 70 | 71 | enablePlugins(SbtPlugin) 72 | 73 | scriptedLaunchOpts ++= Seq("-Dplugin.version=" + version.value) 74 | scriptedBufferLog := true 75 | // This sbt version is necessary for CI to work on windows with 76 | // scripted tests, see https://github.com/sbt/sbt/pull/7087 77 | scriptedSbt := { 78 | scalaBinaryVersion.value match { 79 | case "2.12" => 80 | "1.10.2" 81 | case _ => 82 | scriptedSbt.value 83 | } 84 | } 85 | 86 | ThisBuild / homepage := Some(url("https://github.com/sbt/sbt-github-actions")) 87 | ThisBuild / startYear := Some(2020) 88 | ThisBuild / dynverSonatypeSnapshots := true 89 | ThisBuild / developers := List( 90 | Developer( 91 | id = "armanbilge", 92 | name = "Arman Bilge", 93 | email = "@armanbilge", 94 | url = url("https://github.com/armanbilge") 95 | ), 96 | Developer( 97 | id = "djspiewak", 98 | name = "Daniel Spiewak", 99 | email = "@djspiewak", 100 | url = url("https://github.com/djspiewak") 101 | ), 102 | Developer( 103 | id = "eed3si9n", 104 | name = "Eugene Yokota", 105 | email = "@eed3si9n", 106 | url = url("https://github.com/eed3si9n") 107 | ), 108 | Developer( 109 | id = "mdedetrich", 110 | name = "Matthew de Detrich", 111 | email = "mdedetrich@gmail.com", 112 | url = url("https://github.com/mdedetrich") 113 | ) 114 | ) 115 | ThisBuild / description := "An sbt plugin which makes it easier to build with GitHub Actions" 116 | ThisBuild / licenses := List("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")) 117 | ThisBuild / pomIncludeRepository := { _ => 118 | false 119 | } 120 | ThisBuild / publishMavenStyle := true 121 | Global / excludeLintKeys ++= Set(pomIncludeRepository, publishMavenStyle) 122 | -------------------------------------------------------------------------------- /libraries.sbt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | libraryDependencies += "org.yaml" % "snakeyaml" % "2.3" 18 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.0 2 | -------------------------------------------------------------------------------- /project/libraries.sbt: -------------------------------------------------------------------------------- 1 | ../libraries.sbt -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Refer to the symlinked `sbt-github-actions` sources in the meta-meta-build, 18 | // necessary hack to avoid an infinite project loading recursion. 19 | val sbtGithubActionsSources = ProjectRef(file("project"), "sbtGithubActionsSources") 20 | 21 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.0") 22 | -------------------------------------------------------------------------------- /project/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.2 2 | -------------------------------------------------------------------------------- /project/project/build.sbt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Compile an SbtPlugin from the symlinked sources of `sbt-github-actions`. 18 | lazy val sbtGithubActionsSources = project 19 | .in(file(".")) 20 | .enablePlugins(SbtPlugin) 21 | -------------------------------------------------------------------------------- /project/project/libraries.sbt: -------------------------------------------------------------------------------- 1 | ../../libraries.sbt -------------------------------------------------------------------------------- /project/project/src/main: -------------------------------------------------------------------------------- 1 | ../../src/main -------------------------------------------------------------------------------- /project/src/main: -------------------------------------------------------------------------------- 1 | ../../src/main -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/GenerativeKeys.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import sbt._ 20 | 21 | import scala.concurrent.duration.FiniteDuration 22 | 23 | trait GenerativeKeys { 24 | 25 | lazy val githubWorkflowGenerate = taskKey[Unit]("Generates (and overwrites if extant) a ci.yml and clean.yml actions description according to configuration") 26 | lazy val githubWorkflowCheck = taskKey[Unit]("Checks to see if the ci.yml and clean.yml files are equivalent to what would be generated and errors if otherwise") 27 | 28 | lazy val githubWorkflowDir = settingKey[File]("Where to place the workflow directory which contains the generated ci.yml and clean.yml files. (default: baseDirectory.value / \".github\")") 29 | lazy val githubWorkflowGeneratedCI = settingKey[Seq[WorkflowJob]]("The sequence of jobs which will make up the generated ci workflow (ci.yml)") 30 | lazy val githubWorkflowGeneratedUploadSteps = settingKey[Seq[WorkflowStep]]("The sequence of steps used to upload intermediate build artifacts for an adjacent job") 31 | lazy val githubWorkflowGeneratedDownloadSteps = settingKey[Seq[WorkflowStep]]("The sequence of steps used to download intermediate build artifacts published by an adjacent job") 32 | lazy val githubWorkflowGeneratedCacheSteps = settingKey[Seq[WorkflowStep]]("The sequence of steps used to configure caching for ivy, sbt, and coursier") 33 | 34 | lazy val githubWorkflowSbtCommand = settingKey[String]("The command which invokes sbt (default: sbt)") 35 | lazy val githubWorkflowUseSbtThinClient = settingKey[Boolean]("Whether to use sbt's native thin client, default is false since this can cause issues (see https://github.com/sbt/sbt/issues/6468)") 36 | lazy val githubWorkflowIncludeClean = settingKey[Boolean]("Whether to include the clean.yml file (default: true)") 37 | 38 | lazy val githubWorkflowBuildMatrixFailFast = settingKey[Option[Boolean]]("Whether or not to enable the fail-fast strategy (default: None/Enabled)") 39 | lazy val githubWorkflowBuildMatrixAdditions = settingKey[Map[String, List[String]]]("A map of additional matrix dimensions for the build job. Each list should be non-empty. (default: {})") 40 | lazy val githubWorkflowBuildMatrixInclusions = settingKey[Seq[MatrixInclude]]("A list of matrix inclusions (default: [])") 41 | lazy val githubWorkflowBuildMatrixExclusions = settingKey[Seq[MatrixExclude]]("A list of matrix exclusions (default: [])") 42 | lazy val githubWorkflowBuildRunsOnExtraLabels = settingKey[Seq[String]]("A list of additional labels to append to each run of the matrix executions") 43 | 44 | lazy val githubWorkflowBuildPreamble = settingKey[Seq[WorkflowStep]]("A list of steps to insert after base setup but before compiling and testing (default: [])") 45 | lazy val githubWorkflowBuildPostamble = settingKey[Seq[WorkflowStep]]("A list of steps to insert after comping and testing but before the end of the build job (default: [])") 46 | lazy val githubWorkflowBuildSbtStepPreamble =settingKey[Seq[String]](s"Commands automatically prepended to a WorkflowStep.Sbt (default: ['++$${{ matrix.scala }}'])") 47 | lazy val githubWorkflowBuildTimeout = settingKey[Option[FiniteDuration]]("The maximum duration to let the build job run before GitHub automatically cancels it. (default: None)") 48 | lazy val githubWorkflowBuild = settingKey[Seq[WorkflowStep]]("A sequence of workflow steps which compile and test the project (default: [Sbt(List(\"test\"))])") 49 | 50 | lazy val githubWorkflowPublishPreamble = settingKey[Seq[WorkflowStep]]("A list of steps to insert after base setup but before publishing (default: [])") 51 | lazy val githubWorkflowPublishPostamble = settingKey[Seq[WorkflowStep]]("A list of steps to insert after publication but before the end of the publish job (default: [])") 52 | lazy val githubWorkflowPublishSbtStepPreamble =settingKey[Seq[String]]("Commands automatically prepended to a WorkflowStep.Sbt during publishing (default: [''])") 53 | lazy val githubWorkflowPublish = settingKey[Seq[WorkflowStep]]("A sequence workflow steps which publishes the project (default: [Sbt(List(\"+publish\"))])") 54 | lazy val githubWorkflowPublishTargetBranches = settingKey[Seq[RefPredicate]]("A set of branch predicates which will be applied to determine whether the current branch gets a publication stage; if empty, publish will be skipped entirely (default: [== main])") 55 | lazy val githubWorkflowPublishCond = settingKey[Option[String]]("A set of conditionals to apply to the publish job to further restrict its run (default: [])") 56 | lazy val githubWorkflowPublishTimeout = settingKey[Option[FiniteDuration]]("The maximum duration to let the publish job run before GitHub automatically cancels it. (default: None)") 57 | 58 | lazy val githubWorkflowJavaVersions = settingKey[Seq[JavaSpec]]("A list of Java versions to be used for the build job. The publish job will use the *first* of these versions. (default: [zulu@8])") 59 | lazy val githubWorkflowScalaVersions = settingKey[Seq[String]]("A list of Scala versions on which to build the project (default: crossScalaVersions.value)") 60 | lazy val githubWorkflowOSes = settingKey[Seq[String]]("A list of OS names (default: [ubuntu-latest])") 61 | 62 | lazy val githubWorkflowDependencyPatterns = settingKey[Seq[String]]("A list of file globes within the project which affect dependency information (default: [**/*.sbt, project/build.properties])") 63 | lazy val githubWorkflowTargetBranches = settingKey[Seq[String]]("A list of branch patterns on which to trigger push and PR builds (default: [*])") 64 | lazy val githubWorkflowTargetTags = settingKey[Seq[String]]("A list of tag patterns on which to trigger push builds (default: [])") 65 | lazy val githubWorkflowTargetPaths = settingKey[Paths]("Paths which will match modified files for `push` and `pull_request` event types to trigger the workflow. May be `Paths.None`, `Paths.Include(patterns)`, or `Paths.Ignore(patterns)`. `Paths.Include` may include negative patterns. Defaults to `Paths.None`.") 66 | lazy val githubWorkflowPREventTypes = settingKey[Seq[PREventType]]("A list of pull request event types which will be used to trigger builds (default: [opened, synchronize, reopened])") 67 | 68 | lazy val githubWorkflowArtifactUpload = settingKey[Boolean]("Controls whether or not to upload target directories in the event that multiple jobs are running sequentially. Can be set on a per-project basis (default: true)") 69 | lazy val githubWorkflowJobSetup = settingKey[Seq[WorkflowStep]]("The automatically-generated checkout, setup, and cache steps which are common to all jobs which touch the build (default: autogenerated)") 70 | 71 | lazy val githubWorkflowEnv = settingKey[Map[String, String]](s"A map of static environment variable assignments global to the workflow (default: { GITHUB_TOKEN: $${{ secrets.GITHUB_TOKEN }} })") 72 | lazy val githubWorkflowPermissions = settingKey[Option[Permissions]](s"Permissions to use for the global workflow (default: None)") 73 | lazy val githubWorkflowAddedJobs = settingKey[Seq[WorkflowJob]]("A list of additional jobs to add to the CI workflow (default: [])") 74 | 75 | // Windows related settings 76 | lazy val githubWorkflowWindowsPagefileFix = settingKey[Option[windows.PagefileFix]]("If defined adds a workflow step that increases the pagefile size for Windows workflow's which can sometimes necessary in certain cases, i.e. when using scala-native (default: windows.PagefileFix(2GB,8GB))") 77 | } 78 | 79 | object GenerativeKeys extends GenerativeKeys 80 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/GenerativePlugin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import sbt.Keys._ 20 | import sbt.{given, _} 21 | 22 | import java.nio.file.FileSystems 23 | import scala.concurrent.duration.FiniteDuration 24 | 25 | object GenerativePlugin extends AutoPlugin { 26 | 27 | override def requires = plugins.JvmPlugin 28 | override def trigger = allRequirements 29 | 30 | object autoImport extends GenerativeKeys { 31 | type WorkflowJob = sbtghactions.WorkflowJob 32 | val WorkflowJob = sbtghactions.WorkflowJob 33 | 34 | type JobContainer = sbtghactions.JobContainer 35 | val JobContainer = sbtghactions.JobContainer 36 | 37 | type WorkflowStep = sbtghactions.WorkflowStep 38 | val WorkflowStep = sbtghactions.WorkflowStep 39 | 40 | type RefPredicate = sbtghactions.RefPredicate 41 | val RefPredicate = sbtghactions.RefPredicate 42 | 43 | type Ref = sbtghactions.Ref 44 | val Ref = sbtghactions.Ref 45 | 46 | type UseRef = sbtghactions.UseRef 47 | val UseRef = sbtghactions.UseRef 48 | 49 | type PREventType = sbtghactions.PREventType 50 | val PREventType = sbtghactions.PREventType 51 | 52 | type MatrixInclude = sbtghactions.MatrixInclude 53 | val MatrixInclude = sbtghactions.MatrixInclude 54 | 55 | type MatrixExclude = sbtghactions.MatrixExclude 56 | val MatrixExclude = sbtghactions.MatrixExclude 57 | 58 | type Paths = sbtghactions.Paths 59 | val Paths = sbtghactions.Paths 60 | 61 | type JavaSpec = sbtghactions.JavaSpec 62 | val JavaSpec = sbtghactions.JavaSpec 63 | 64 | type Permissions = sbtghactions.Permissions 65 | val Permissions = sbtghactions.Permissions 66 | 67 | type PermissionScope = sbtghactions.PermissionScope 68 | val PermissionScope = sbtghactions.PermissionScope 69 | 70 | type PermissionValue = sbtghactions.PermissionValue 71 | val PermissionValue = sbtghactions.PermissionValue 72 | 73 | type Graalvm = sbtghactions.Graalvm 74 | val Graalvm = sbtghactions.Graalvm 75 | } 76 | 77 | import autoImport._ 78 | 79 | private def indent(output: String, level: Int): String = { 80 | val space = (0 until level * 2).map(_ => ' ').mkString 81 | (space + output.replace("\n", s"\n$space")).replaceAll("""\n[ ]+\n""", "\n\n") 82 | } 83 | 84 | private def isSafeString(str: String): Boolean = 85 | !(str.indexOf(':') >= 0 || // pretend colon is illegal everywhere for simplicity 86 | str.indexOf('#') >= 0 || // same for comment 87 | str.indexOf('!') == 0 || 88 | str.indexOf('*') == 0 || 89 | str.indexOf('-') == 0 || 90 | str.indexOf('?') == 0 || 91 | str.indexOf('{') == 0 || 92 | str.indexOf('}') == 0 || 93 | str.indexOf('[') == 0 || 94 | str.indexOf(']') == 0 || 95 | str.indexOf(',') == 0 || 96 | str.indexOf('|') == 0 || 97 | str.indexOf('>') == 0 || 98 | str.indexOf('@') == 0 || 99 | str.indexOf('`') == 0 || 100 | str.indexOf('"') == 0 || 101 | str.indexOf('\'') == 0 || 102 | str.indexOf('&') == 0) 103 | 104 | private def wrap(str: String): String = 105 | if (str.indexOf('\n') >= 0) 106 | "|\n" + indent(str, 1) 107 | else if (isSafeString(str)) 108 | str 109 | else 110 | s"'${str.replace("'", "''")}'" 111 | 112 | def compileList(items: List[String], level: Int): String = { 113 | val rendered = items.map(wrap) 114 | if (rendered.map(_.length).sum < 40) // just arbitrarily... 115 | rendered.mkString(" [", ", ", "]") 116 | else 117 | "\n" + indent(rendered.map("- " + _).mkString("\n"), level) 118 | } 119 | 120 | def compileListOfSimpleDicts(items: List[Map[String, String]]): String = 121 | items map { dict => 122 | val rendered = dict map { 123 | case (key, value) => s"$key: $value" 124 | } mkString "\n" 125 | 126 | "-" + indent(rendered, 1).substring(1) 127 | } mkString "\n" 128 | 129 | def compilePREventType(tpe: PREventType): String = { 130 | import PREventType._ 131 | 132 | tpe match { 133 | case Assigned => "assigned" 134 | case Unassigned => "unassigned" 135 | case Labeled => "labeled" 136 | case Unlabeled => "unlabeled" 137 | case Opened => "opened" 138 | case Edited => "edited" 139 | case Closed => "closed" 140 | case Reopened => "reopened" 141 | case Synchronize => "synchronize" 142 | case ReadyForReview => "ready_for_review" 143 | case Locked => "locked" 144 | case Unlocked => "unlocked" 145 | case ReviewRequested => "review_requested" 146 | case ReviewRequestRemoved => "review_request_removed" 147 | } 148 | } 149 | 150 | def compileRef(ref: Ref): String = ref match { 151 | case Ref.Branch(name) => s"refs/heads/$name" 152 | case Ref.Tag(name) => s"refs/tags/$name" 153 | } 154 | 155 | def compileBranchPredicate(target: String, pred: RefPredicate): String = pred match { 156 | case RefPredicate.Equals(ref) => 157 | s"$target == '${compileRef(ref)}'" 158 | 159 | case RefPredicate.Contains(Ref.Tag(name)) => 160 | s"(startsWith($target, 'refs/tags/') && contains($target, '$name'))" 161 | 162 | case RefPredicate.Contains(Ref.Branch(name)) => 163 | s"(startsWith($target, 'refs/heads/') && contains($target, '$name'))" 164 | 165 | case RefPredicate.StartsWith(ref) => 166 | s"startsWith($target, '${compileRef(ref)}')" 167 | 168 | case RefPredicate.EndsWith(Ref.Tag(name)) => 169 | s"(startsWith($target, 'refs/tags/') && endsWith($target, '$name'))" 170 | 171 | case RefPredicate.EndsWith(Ref.Branch(name)) => 172 | s"(startsWith($target, 'refs/heads/') && endsWith($target, '$name'))" 173 | } 174 | 175 | def compileEnvironment(environment: JobEnvironment): String = 176 | environment.url match { 177 | case Some(url) => 178 | val fields = s"""name: ${wrap(environment.name)} 179 | |url: ${wrap(url.toString)}""".stripMargin 180 | s"""environment: 181 | |${indent(fields, 1)}""".stripMargin 182 | case None => 183 | s"environment: ${wrap(environment.name)}" 184 | } 185 | 186 | def compileEnv(env: Map[String, String], prefix: String = "env", ignoreWhiteSpace: Boolean = false): String = 187 | if (env.isEmpty) { 188 | "" 189 | } else { 190 | val rendered = env map { 191 | case (key, value) => 192 | val whitSpaceValidation = if(ignoreWhiteSpace) false else key.indexOf(' ') >= 0 193 | 194 | if (!isSafeString(key) || whitSpaceValidation) 195 | sys.error(s"'$key' is not a valid environment variable name") 196 | 197 | s"""$key: ${wrap(value)}""" 198 | } 199 | s"""$prefix: 200 | ${indent(rendered.mkString("\n"), 1)}""" 201 | } 202 | 203 | def compilePermissionScope(permissionScope: PermissionScope): String = permissionScope match { 204 | case PermissionScope.Actions => "actions" 205 | case PermissionScope.Attestations => "attestations" 206 | case PermissionScope.Checks => "checks" 207 | case PermissionScope.Contents => "contents" 208 | case PermissionScope.Deployments => "deployments" 209 | case PermissionScope.IdToken => "id-token" 210 | case PermissionScope.Issues => "issues" 211 | case PermissionScope.Discussions => "discussions" 212 | case PermissionScope.Packages => "packages" 213 | case PermissionScope.Pages => "pages" 214 | case PermissionScope.PullRequests => "pull-requests" 215 | case PermissionScope.RepositoryProjects => "repository-projects" 216 | case PermissionScope.SecurityEvents => "security-events" 217 | case PermissionScope.Statuses => "statuses" 218 | } 219 | 220 | def compilePermissionsValue(permissionValue: PermissionValue): String = permissionValue match { 221 | case PermissionValue.Read => "read" 222 | case PermissionValue.Write => "write" 223 | case PermissionValue.None => "none" 224 | } 225 | 226 | def compilePermissions(permissions: Option[Permissions]): String = { 227 | permissions match { 228 | case Some(perms) => 229 | val rendered = perms match { 230 | case Permissions.ReadAll => " read-all" 231 | case Permissions.WriteAll => " write-all" 232 | case Permissions.None => " {}" 233 | case Permissions.Specify(permMap) => 234 | val map = permMap.map{ 235 | case (key, value) => 236 | s"${compilePermissionScope(key)}: ${compilePermissionsValue(value)}" 237 | } 238 | "\n" + indent(map.mkString("\n"), 1) 239 | } 240 | s"permissions:$rendered" 241 | 242 | case None => "" 243 | } 244 | } 245 | 246 | def compileTimeout(timeout: Option[FiniteDuration], prefix: String = ""): String = { 247 | timeout.map(_.toMinutes.toString).map(s"${prefix}timeout-minutes: " + _ + "\n").getOrElse("") 248 | } 249 | 250 | def compileStep( 251 | step: WorkflowStep, 252 | sbt: String, 253 | sbtStepPreamble: List[String] = WorkflowStep.DefaultSbtStepPreamble, 254 | declareShell: Boolean = false 255 | ): String = { 256 | import WorkflowStep._ 257 | 258 | val renderedName = step.name.map(wrap).map("name: " + _ + "\n").getOrElse("") 259 | val renderedId = step.id.map(wrap).map("id: " + _ + "\n").getOrElse("") 260 | val renderedCond = step.cond.map(wrap).map("if: " + _ + "\n").getOrElse("") 261 | val renderedShell = if (declareShell) "shell: bash\n" else "" 262 | val renderedTimeout = compileTimeout(step.timeout) 263 | 264 | val renderedEnvPre = compileEnv(step.env) 265 | val renderedEnv = if (renderedEnvPre.isEmpty) 266 | "" 267 | else 268 | renderedEnvPre + "\n" 269 | 270 | val preamblePre = renderedName + renderedId + renderedCond + renderedEnv + renderedTimeout 271 | 272 | val preamble = if (preamblePre.isEmpty) 273 | "" 274 | else 275 | preamblePre 276 | 277 | val body = step match { 278 | case run: Run => 279 | renderRunBody(run.commands, run.params, renderedShell) 280 | 281 | case sbtStep: Sbt => 282 | import sbtStep.commands 283 | 284 | val sbtClientMode = sbt.matches("""sbt.* --client($| .*)""") 285 | val safeCommands = if (sbtClientMode) 286 | s"'${(sbtStepPreamble ::: commands).mkString("; ")}'" 287 | else (sbtStepPreamble ::: commands).map { c => 288 | if (c.indexOf(' ') >= 0) 289 | s"'$c'" 290 | else 291 | c 292 | }.mkString(" ") 293 | 294 | renderRunBody( 295 | commands = List(s"$sbt $safeCommands"), 296 | params = sbtStep.params, 297 | renderedShell = renderedShell 298 | ) 299 | 300 | case use: Use => 301 | import use.{ref, params} 302 | 303 | val decl = ref match { 304 | case UseRef.Public(owner, repo, ref) => 305 | s"uses: $owner/$repo@$ref" 306 | 307 | case UseRef.Local(path) => 308 | val cleaned = if (path.startsWith("./")) 309 | path 310 | else 311 | "./" + path 312 | 313 | s"uses: $cleaned" 314 | 315 | case UseRef.Docker(image, tag, Some(host)) => 316 | s"uses: docker://$host/$image:$tag" 317 | 318 | case UseRef.Docker(image, tag, None) => 319 | s"uses: docker://$image:$tag" 320 | } 321 | 322 | decl + renderParams(params) 323 | } 324 | 325 | indent(preamble + body, 1).updated(0, '-') 326 | } 327 | 328 | def renderRunBody(commands: List[String], params: Map[String, String], renderedShell: String) = 329 | renderedShell + "run: " + wrap(commands.mkString("\n")) + renderParams(params) 330 | 331 | def renderParams(params: Map[String, String]): String = { 332 | val renderedParamsPre = compileEnv(params, prefix = "with", ignoreWhiteSpace = true) 333 | val renderedParams = if (renderedParamsPre.isEmpty) 334 | "" 335 | else 336 | "\n" + renderedParamsPre 337 | 338 | renderedParams 339 | } 340 | 341 | 342 | def compileJob(job: WorkflowJob, sbt: String): String = { 343 | val renderedNeeds = if (job.needs.isEmpty) 344 | "" 345 | else 346 | s"\nneeds: [${job.needs.mkString(", ")}]" 347 | 348 | val renderedEnvironment = job.environment.map(compileEnvironment).map("\n" + _).getOrElse("") 349 | val renderedTimeout = compileTimeout(job.timeout, prefix = "\n") 350 | 351 | val renderedCond = job.cond.map(wrap).map("\nif: " + _).getOrElse("") 352 | 353 | val renderedContainer = job.container match { 354 | case Some(JobContainer(image, credentials, env, volumes, ports, options)) => 355 | if (credentials.isEmpty && env.isEmpty && volumes.isEmpty && ports.isEmpty && options.isEmpty) { 356 | "\n" + s"container: ${wrap(image)}" 357 | } else { 358 | val renderedImage = s"image: ${wrap(image)}" 359 | 360 | val renderedCredentials = credentials match { 361 | case Some((username, password)) => 362 | s"\ncredentials:\n${indent(s"username: ${wrap(username)}\npassword: ${wrap(password)}", 1)}" 363 | 364 | case None => 365 | "" 366 | } 367 | 368 | val renderedEnv = if (!env.isEmpty) 369 | "\n" + compileEnv(env) 370 | else 371 | "" 372 | 373 | val renderedVolumes = if (!volumes.isEmpty) 374 | s"\nvolumes:${compileList(volumes.toList map { case (l, r) => s"$l:$r" }, 1)}" 375 | else 376 | "" 377 | 378 | val renderedPorts = if (!ports.isEmpty) 379 | s"\nports:${compileList(ports.map(_.toString), 1)}" 380 | else 381 | "" 382 | 383 | val renderedOptions = if (!options.isEmpty) 384 | s"\noptions: ${wrap(options.mkString(" "))}" 385 | else 386 | "" 387 | 388 | s"\ncontainer:\n${indent(renderedImage + renderedCredentials + renderedEnv + renderedVolumes + renderedPorts + renderedOptions, 1)}" 389 | } 390 | 391 | case None => 392 | "" 393 | } 394 | 395 | val renderedEnvPre = compileEnv(job.env) 396 | val renderedEnv = if (renderedEnvPre.isEmpty) 397 | "" 398 | else 399 | "\n" + renderedEnvPre 400 | 401 | val renderedPermPre = compilePermissions(job.permissions) 402 | val renderedPerm = if (renderedPermPre.isEmpty) 403 | "" 404 | else 405 | "\n" + renderedPermPre 406 | 407 | List("include", "exclude") foreach { key => 408 | if (job.matrixAdds.contains(key)) { 409 | sys.error(s"key `$key` is reserved and cannot be used in an Actions matrix definition") 410 | } 411 | } 412 | 413 | val renderedMatricesPre = job.matrixAdds map { 414 | case (key, values) => s"$key: ${values.map(wrap).mkString("[", ", ", "]")}" 415 | } mkString "\n" 416 | 417 | // TODO refactor all of this stuff to use whitelist instead 418 | val whitelist = Map("os" -> job.oses, "scala" -> job.scalas, "java" -> job.javas.map(_.render)) ++ job.matrixAdds 419 | 420 | def checkMatching(matching: Map[String, String]): Unit = { 421 | matching foreach { 422 | case (key, value) => 423 | if (!whitelist.contains(key)) { 424 | sys.error(s"inclusion key `$key` was not found in matrix") 425 | } 426 | 427 | if (!whitelist(key).contains(value)) { 428 | sys.error(s"inclusion key `$key` was present in matrix, but value `$value` was not in ${whitelist(key)}") 429 | } 430 | } 431 | } 432 | 433 | val renderedIncludesPre = if (job.matrixIncs.isEmpty) { 434 | renderedMatricesPre 435 | } else { 436 | job.matrixIncs.foreach(inc => checkMatching(inc.matching)) 437 | 438 | val rendered = compileListOfSimpleDicts(job.matrixIncs.map(i => i.matching ++ i.additions)) 439 | 440 | val renderedMatrices = if (renderedMatricesPre.isEmpty) 441 | "" 442 | else 443 | renderedMatricesPre + "\n" 444 | 445 | s"${renderedMatrices}include:\n${indent(rendered, 1)}" 446 | } 447 | 448 | val renderedExcludesPre = if (job.matrixExcs.isEmpty) { 449 | renderedIncludesPre 450 | } else { 451 | job.matrixExcs.foreach(exc => checkMatching(exc.matching)) 452 | 453 | val rendered = compileListOfSimpleDicts(job.matrixExcs.map(_.matching)) 454 | 455 | val renderedIncludes = if (renderedIncludesPre.isEmpty) 456 | "" 457 | else 458 | renderedIncludesPre + "\n" 459 | 460 | s"${renderedIncludes}exclude:\n${indent(rendered, 1)}" 461 | } 462 | 463 | val renderedMatrices = if (renderedExcludesPre.isEmpty) 464 | "" 465 | else 466 | "\n" + indent(renderedExcludesPre, 2) 467 | 468 | val declareShell = job.oses.exists(_.contains("windows")) 469 | 470 | val runsOn = if (job.runsOnExtraLabels.isEmpty) 471 | s"$${{ matrix.os }}" 472 | else 473 | job.runsOnExtraLabels.mkString(s"""[ "$${{ matrix.os }}", """, ", ", " ]" ) 474 | 475 | val renderedFailFast = job.matrixFailFast.fold("")("\n fail-fast: " + _) 476 | 477 | val body = s"""name: ${wrap(job.name)}${renderedNeeds}${renderedCond} 478 | strategy:${renderedFailFast} 479 | matrix: 480 | os:${compileList(job.oses, 3)} 481 | scala:${compileList(job.scalas, 3)} 482 | java:${compileList(job.javas.map(_.render), 3)}${renderedMatrices} 483 | runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedTimeout}${renderedPerm}${renderedEnv} 484 | steps: 485 | ${indent(job.steps.map(compileStep(_, sbt, job.sbtStepPreamble, declareShell = declareShell)).mkString("\n\n"), 1)}""" 486 | 487 | s"${job.id}:\n${indent(body, 1)}" 488 | } 489 | 490 | def compileWorkflow( 491 | name: String, 492 | branches: List[String], 493 | tags: List[String], 494 | paths: Paths, 495 | prEventTypes: List[PREventType], 496 | permissions: Option[Permissions], 497 | env: Map[String, String], 498 | jobs: List[WorkflowJob], 499 | sbt: String) 500 | : String = { 501 | 502 | val renderedPermissionsPre = compilePermissions(permissions) 503 | val renderedEnvPre = compileEnv(env) 504 | val renderedEnv = if (renderedEnvPre.isEmpty) 505 | "" 506 | else 507 | renderedEnvPre + "\n\n" 508 | val renderedPerm = if (renderedPermissionsPre.isEmpty) 509 | "" 510 | else 511 | renderedPermissionsPre + "\n\n" 512 | 513 | val renderedTypesPre = prEventTypes.map(compilePREventType).mkString("[", ", ", "]") 514 | val renderedTypes = if (prEventTypes.sortBy(_.toString) == PREventType.Defaults) 515 | "" 516 | else 517 | "\n" + indent("types: " + renderedTypesPre, 2) 518 | 519 | val renderedTags = if (tags.isEmpty) 520 | "" 521 | else 522 | s""" 523 | tags: [${tags.map(wrap).mkString(", ")}]""" 524 | 525 | val renderedPaths = paths match { 526 | case Paths.None => 527 | "" 528 | case Paths.Include(paths) => 529 | "\n" + indent(s"""paths: [${paths.map(wrap).mkString(", ")}]""", 2) 530 | case Paths.Ignore(paths) => 531 | "\n" + indent(s"""paths-ignore: [${paths.map(wrap).mkString(", ")}]""", 2) 532 | } 533 | 534 | s"""# This file was automatically generated by sbt-github-actions using the 535 | # githubWorkflowGenerate task. You should add and commit this file to 536 | # your git repository. It goes without saying that you shouldn't edit 537 | # this file by hand! Instead, if you wish to make changes, you should 538 | # change your sbt build configuration to revise the workflow description 539 | # to meet your needs, then regenerate this file. 540 | 541 | name: ${wrap(name)} 542 | 543 | on: 544 | pull_request: 545 | branches: [${branches.map(wrap).mkString(", ")}]$renderedTypes$renderedPaths 546 | push: 547 | branches: [${branches.map(wrap).mkString(", ")}]$renderedTags$renderedPaths 548 | 549 | ${renderedPerm}${renderedEnv}jobs: 550 | ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} 551 | """ 552 | } 553 | 554 | val settingDefaults = Seq( 555 | githubWorkflowSbtCommand := "sbt", 556 | githubWorkflowIncludeClean := true, 557 | // This is currently set to false because of https://github.com/sbt/sbt/issues/6468. When a new SBT version is 558 | // released that fixes this issue then check for that SBT version (or higher) and set to true. 559 | githubWorkflowUseSbtThinClient := false, 560 | 561 | githubWorkflowBuildMatrixFailFast := None, 562 | githubWorkflowBuildMatrixAdditions := Map(), 563 | githubWorkflowBuildMatrixInclusions := Seq(), 564 | githubWorkflowBuildMatrixExclusions := Seq(), 565 | githubWorkflowBuildRunsOnExtraLabels := Seq(), 566 | 567 | githubWorkflowBuildPreamble := Seq(), 568 | githubWorkflowBuildPostamble := Seq(), 569 | githubWorkflowBuildSbtStepPreamble := WorkflowStep.DefaultSbtStepPreamble, 570 | githubWorkflowBuildTimeout := None, 571 | githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("test"), name = Some("Build project"))), 572 | 573 | githubWorkflowPublishPreamble := Seq(), 574 | githubWorkflowPublishPostamble := Seq(), 575 | githubWorkflowPublishSbtStepPreamble := Seq(), 576 | githubWorkflowPublish := Seq(WorkflowStep.Sbt(List("+publish"), name = Some("Publish project"))), 577 | githubWorkflowPublishTargetBranches := Seq(RefPredicate.Equals(Ref.Branch("main"))), 578 | githubWorkflowPublishCond := None, 579 | githubWorkflowPublishTimeout := None, 580 | 581 | githubWorkflowJavaVersions := Seq(JavaSpec.zulu("8")), 582 | githubWorkflowScalaVersions := crossScalaVersions.value, 583 | githubWorkflowOSes := Seq("ubuntu-latest"), 584 | githubWorkflowDependencyPatterns := Seq("**/*.sbt", "project/build.properties"), 585 | githubWorkflowTargetBranches := Seq("**"), 586 | githubWorkflowTargetTags := Seq(), 587 | githubWorkflowTargetPaths := Paths.None, 588 | 589 | githubWorkflowEnv := Map("GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), 590 | githubWorkflowPermissions := None, 591 | githubWorkflowAddedJobs := Seq(), 592 | githubWorkflowWindowsPagefileFix := Some( 593 | windows.PagefileFix("2GB", "8GB") 594 | ) 595 | ) 596 | 597 | private lazy val internalTargetAggregation = settingKey[Seq[File]]("Aggregates target directories from all subprojects") 598 | 599 | private val windowsGuard = Some("contains(runner.os, 'windows')") 600 | 601 | private val PlatformSep = FileSystems.getDefault.getSeparator 602 | private def normalizeSeparators(pathStr: String): String = { 603 | pathStr.replace(PlatformSep, "/") // *force* unix separators 604 | } 605 | 606 | private val pathStrs = Def setting { 607 | val base = (ThisBuild / baseDirectory).value.toPath 608 | 609 | internalTargetAggregation.value map { file => 610 | val path = file.toPath 611 | 612 | if (path.isAbsolute) 613 | normalizeSeparators(base.relativize(path).toString) 614 | else 615 | normalizeSeparators(path.toString) 616 | } 617 | } 618 | 619 | override def globalSettings = Seq( 620 | internalTargetAggregation := Seq(), 621 | githubWorkflowArtifactUpload := true) 622 | 623 | override def buildSettings = settingDefaults ++ Seq( 624 | githubWorkflowPREventTypes := PREventType.Defaults, 625 | 626 | githubWorkflowGeneratedUploadSteps := { 627 | if (githubWorkflowArtifactUpload.value) { 628 | val sanitized = pathStrs.value map { str => 629 | if (str.indexOf(' ') >= 0) // TODO be less naive 630 | s"'$str'" 631 | else 632 | str 633 | } 634 | 635 | val tar = WorkflowStep.Run( 636 | List(s"tar cf targets.tar ${sanitized.mkString(" ")} project/target"), 637 | name = Some("Compress target directories")) 638 | 639 | val upload = WorkflowStep.Use( 640 | UseRef.Public( 641 | "actions", 642 | "upload-artifact", 643 | "v4"), 644 | name = Some(s"Upload target directories"), 645 | params = Map( 646 | "name" -> s"target-$${{ matrix.os }}-$${{ matrix.scala }}-$${{ matrix.java }}", 647 | "path" -> "targets.tar")) 648 | 649 | Seq(tar, upload) 650 | } else { 651 | Seq() 652 | } 653 | }, 654 | 655 | githubWorkflowGeneratedDownloadSteps := { 656 | val scalas = githubWorkflowScalaVersions.value 657 | 658 | if (githubWorkflowArtifactUpload.value) { 659 | scalas flatMap { v => 660 | val download = WorkflowStep.Use( 661 | UseRef.Public( 662 | "actions", 663 | "download-artifact", 664 | "v4"), 665 | name = Some(s"Download target directories ($v)"), 666 | params = Map( 667 | "name" -> s"target-$${{ matrix.os }}-$v-$${{ matrix.java }}")) 668 | 669 | val untar = WorkflowStep.Run( 670 | List( 671 | "tar xf targets.tar", 672 | "rm targets.tar"), 673 | name = Some(s"Inflate target directories ($v)")) 674 | 675 | Seq(download, untar) 676 | } 677 | } else { 678 | Seq() 679 | } 680 | }, 681 | 682 | githubWorkflowGeneratedCacheSteps := Nil, 683 | 684 | githubWorkflowJobSetup := { 685 | val autoCrlfOpt = if (githubWorkflowOSes.value.exists(_.contains("windows"))) { 686 | val optionalPagefileFix = githubWorkflowWindowsPagefileFix.value.map(pageFileFix => 687 | WorkflowStep.Use( 688 | name = Some("Configure pagefile for Windows"), 689 | ref = UseRef.Public("al-cheb", "configure-pagefile-action", "v1.4"), 690 | params = Map( 691 | "minimum-size" -> s"${pageFileFix.minSize}", 692 | "maximum-size" -> s"${pageFileFix.maxSize}" 693 | ) ++ pageFileFix.diskRoot.map(diskRoot => 694 | Map("disk-root" -> s"$diskRoot") 695 | ).getOrElse(Map.empty), 696 | cond = windowsGuard 697 | ) 698 | ).toList 699 | 700 | List( 701 | WorkflowStep.Run( 702 | List("git config --global core.autocrlf false"), 703 | name = Some("Ignore line ending differences in git"), 704 | cond = windowsGuard) 705 | ) ++ optionalPagefileFix 706 | } else { 707 | Nil 708 | } 709 | 710 | autoCrlfOpt ::: 711 | List(WorkflowStep.CheckoutFull) ::: 712 | WorkflowStep.SetupJava(githubWorkflowJavaVersions.value.toList) ::: 713 | List(WorkflowStep.SetupSbt()) ::: 714 | githubWorkflowGeneratedCacheSteps.value.toList 715 | }, 716 | 717 | githubWorkflowGeneratedCI := { 718 | val publicationCondPre = 719 | githubWorkflowPublishTargetBranches.value.map(compileBranchPredicate("github.ref", _)).mkString("(", " || ", ")") 720 | 721 | val publicationCond = githubWorkflowPublishCond.value match { 722 | case Some(cond) => publicationCondPre + " && (" + cond + ")" 723 | case None => publicationCondPre 724 | } 725 | 726 | val uploadStepsOpt = if (githubWorkflowPublishTargetBranches.value.isEmpty && githubWorkflowAddedJobs.value.isEmpty) 727 | Nil 728 | else 729 | githubWorkflowGeneratedUploadSteps.value.toList 730 | 731 | val publishJobOpt = Seq( 732 | WorkflowJob( 733 | "publish", 734 | "Publish Artifacts", 735 | githubWorkflowJobSetup.value.toList ::: 736 | githubWorkflowGeneratedDownloadSteps.value.toList ::: 737 | githubWorkflowPublishPreamble.value.toList ::: 738 | githubWorkflowPublish.value.toList ::: 739 | githubWorkflowPublishPostamble.value.toList, 740 | sbtStepPreamble = githubWorkflowPublishSbtStepPreamble.value.toList, 741 | oses = List(githubWorkflowOSes.value.headOption.getOrElse("ubuntu-latest")), 742 | cond = Some(s"github.event_name != 'pull_request' && $publicationCond"), 743 | scalas = List(scalaVersion.value), 744 | javas = List(githubWorkflowJavaVersions.value.head), 745 | needs = List("build"), 746 | timeout = githubWorkflowPublishTimeout.value)).filter(_ => !githubWorkflowPublishTargetBranches.value.isEmpty) 747 | 748 | Seq( 749 | WorkflowJob( 750 | "build", 751 | "Build and Test", 752 | githubWorkflowJobSetup.value.toList ::: 753 | githubWorkflowBuildPreamble.value.toList ::: 754 | WorkflowStep.Sbt( 755 | List("githubWorkflowCheck"), 756 | name = Some("Check that workflows are up to date")) :: 757 | githubWorkflowBuild.value.toList ::: 758 | githubWorkflowBuildPostamble.value.toList ::: 759 | uploadStepsOpt, 760 | sbtStepPreamble = githubWorkflowBuildSbtStepPreamble.value.toList, 761 | oses = githubWorkflowOSes.value.toList, 762 | scalas = githubWorkflowScalaVersions.value.toList, 763 | javas = githubWorkflowJavaVersions.value.toList, 764 | matrixFailFast = githubWorkflowBuildMatrixFailFast.value, 765 | matrixAdds = githubWorkflowBuildMatrixAdditions.value, 766 | matrixIncs = githubWorkflowBuildMatrixInclusions.value.toList, 767 | matrixExcs = githubWorkflowBuildMatrixExclusions.value.toList, 768 | runsOnExtraLabels = githubWorkflowBuildRunsOnExtraLabels.value.toList, 769 | timeout = githubWorkflowBuildTimeout.value)) ++ publishJobOpt ++ githubWorkflowAddedJobs.value 770 | }) 771 | 772 | private val generateCiContents = Def task { 773 | val sbt = if (githubWorkflowUseSbtThinClient.value) { 774 | githubWorkflowSbtCommand.value + " --client" 775 | } else { 776 | githubWorkflowSbtCommand.value 777 | } 778 | compileWorkflow( 779 | "Continuous Integration", 780 | githubWorkflowTargetBranches.value.toList, 781 | githubWorkflowTargetTags.value.toList, 782 | githubWorkflowTargetPaths.value, 783 | githubWorkflowPREventTypes.value.toList, 784 | githubWorkflowPermissions.value, 785 | githubWorkflowEnv.value, 786 | githubWorkflowGeneratedCI.value.toList, 787 | sbt) 788 | } 789 | 790 | private def generateCleanContents(runsOnOs: String) = { 791 | val first = 792 | """|# This file was automatically generated by sbt-github-actions using the 793 | |# githubWorkflowGenerate task. You should add and commit this file to 794 | |# your git repository. It goes without saying that you shouldn't edit 795 | |# this file by hand! Instead, if you wish to make changes, you should 796 | |# change your sbt build configuration to revise the workflow description 797 | |# to meet your needs, then regenerate this file. 798 | | 799 | |name: Clean 800 | | 801 | |on: push 802 | | 803 | |jobs: 804 | | delete-artifacts: 805 | | name: Delete Artifacts 806 | |""".stripMargin 807 | 808 | val last = 809 | """ 810 | | env: 811 | | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 812 | | steps: 813 | | - name: Delete artifacts 814 | | shell: bash {0} 815 | | run: | 816 | | # Customize those three lines with your repository and credentials: 817 | | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 818 | | 819 | | # A shortcut to call GitHub API. 820 | | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 821 | | 822 | | # A temporary file which receives HTTP response headers. 823 | | TMPFILE=$(mktemp) 824 | | 825 | | # An associative array, key: artifact name, value: number of artifacts of that name. 826 | | declare -A ARTCOUNT 827 | | 828 | | # Process all artifacts on this repository, loop on returned "pages". 829 | | URL=$REPO/actions/artifacts 830 | | while [[ -n "$URL" ]]; do 831 | | 832 | | # Get current page, get response headers in a temporary file. 833 | | JSON=$(ghapi --dump-header $TMPFILE "$URL") 834 | | 835 | | # Get URL of next page. Will be empty if we are at the last page. 836 | | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 837 | | rm -f $TMPFILE 838 | | 839 | | # Number of artifacts on this page: 840 | | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 841 | | 842 | | # Loop on all artifacts on this page. 843 | | for ((i=0; $i < $COUNT; i++)); do 844 | | 845 | | # Get name of artifact and count instances of this name. 846 | | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 847 | | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 848 | | 849 | | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 850 | | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 851 | | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 852 | | ghapi -X DELETE $REPO/actions/artifacts/$id 853 | | done 854 | | done 855 | |""".stripMargin 856 | first + s" runs-on: $runsOnOs" ++ last 857 | } 858 | 859 | private val workflowsDirTask = Def task { 860 | val githubDir = githubWorkflowDir.value 861 | val workflowsDir = githubDir / "workflows" 862 | 863 | if (!githubDir.exists()) { 864 | githubDir.mkdir() 865 | } 866 | 867 | if (!workflowsDir.exists()) { 868 | workflowsDir.mkdir() 869 | } 870 | 871 | workflowsDir 872 | } 873 | 874 | private val ciYmlFile = Def task { 875 | workflowsDirTask.value / "ci.yml" 876 | } 877 | 878 | private val cleanYmlFile = Def task { 879 | workflowsDirTask.value / "clean.yml" 880 | } 881 | 882 | override def projectSettings = Seq( 883 | Global / internalTargetAggregation ++= { 884 | if (githubWorkflowArtifactUpload.value) 885 | Seq(target.value) 886 | else 887 | Seq() 888 | }, 889 | 890 | githubWorkflowGenerate / aggregate := false, 891 | githubWorkflowCheck / aggregate := false, 892 | 893 | githubWorkflowGenerate := { 894 | val ciContents = generateCiContents.value 895 | val includeClean = githubWorkflowIncludeClean.value 896 | val cleanContents = generateCleanContents(githubWorkflowOSes.value.head) 897 | 898 | val ciYml = ciYmlFile.value 899 | val cleanYml = cleanYmlFile.value 900 | 901 | IO.write(ciYml, ciContents) 902 | 903 | if(includeClean) 904 | IO.write(cleanYml, cleanContents) 905 | }, 906 | 907 | githubWorkflowCheck := { 908 | val expectedCiContents = generateCiContents.value 909 | val includeClean = githubWorkflowIncludeClean.value 910 | val expectedCleanContents = generateCleanContents(githubWorkflowOSes.value.head) 911 | 912 | val ciYml = ciYmlFile.value 913 | val cleanYml = cleanYmlFile.value 914 | 915 | val log = state.value.log 916 | 917 | def reportMismatch(file: File, expected: String, actual: String): Unit = { 918 | log.error(s"Expected:\n$expected") 919 | log.error(s"Actual:\n${diff(expected, actual)}") 920 | sys.error(s"${file.getName} does not contain contents that would have been generated by sbt-github-actions; try running githubWorkflowGenerate") 921 | } 922 | 923 | def compare(file: File, expected: String): Unit = { 924 | val actual = IO.read(file) 925 | if (expected != actual) { 926 | reportMismatch(file, expected, actual) 927 | } 928 | } 929 | 930 | compare(ciYml, expectedCiContents) 931 | 932 | if (includeClean) 933 | compare(cleanYml, expectedCleanContents) 934 | }, 935 | githubWorkflowDir := baseDirectory.value / ".github") 936 | 937 | private[sbtghactions] def diff(expected: String, actual: String): String = { 938 | val expectedLines = expected.split("\n", -1) 939 | val actualLines = actual.split("\n", -1) 940 | val (lines, _) = expectedLines.zipAll(actualLines, "", "").foldLeft((Vector.empty[String], false)) { 941 | case ((acc, foundDifference), (expectedLine, actualLine)) if expectedLine == actualLine => 942 | (acc :+ actualLine, foundDifference) 943 | case ((acc, false), ("", actualLine)) => 944 | val previousLineLength = acc.lastOption.map(_.length).getOrElse(0) 945 | val padding = " " * previousLineLength 946 | val highlight = s"$padding^ (additional lines)" 947 | (acc :+ highlight :+ actualLine, true) 948 | case ((acc, false), (_, "")) => 949 | val previousLineLength = acc.lastOption.map(_.length).getOrElse(0) 950 | val padding = " " * previousLineLength 951 | val highlight = s"$padding^ (missing lines)" 952 | (acc :+ highlight, true) 953 | case ((acc, false), (expectedLine, actualLine)) => 954 | val sameCount = expectedLine.zip(actualLine).takeWhile({ case (a, b) => a == b }).length 955 | val padding = " " * sameCount 956 | val highlight = s"$padding^ (different character)" 957 | (acc :+ actualLine :+ highlight, true) 958 | case ((acc, true), (_, "")) => 959 | (acc, true) 960 | case ((acc, true), (_, actualLine)) => 961 | (acc :+ actualLine, true) 962 | } 963 | lines.mkString("\n") 964 | } 965 | } 966 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/GitHubActionsKeys.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import sbt._ 20 | 21 | trait GitHubActionsKeys { 22 | 23 | lazy val githubIsWorkflowBuild = settingKey[Boolean]("Indicates whether or not the current sbt session is running within a GitHub Actions Workflow") 24 | 25 | lazy val githubWorkflowName = settingKey[String]("Contains the name of the currently-running workflow, if defined") 26 | 27 | lazy val githubWorkflowDefinition = settingKey[Map[String, Any]]("The raw (parsed) contents of the workflow manifest file corresponding to this build, recursively converted to Scala") 28 | } 29 | 30 | object GitHubActionsKeys extends GitHubActionsKeys 31 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/GitHubActionsPlugin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import sbt._, Keys._ 20 | import sbt.io.Using 21 | 22 | import org.yaml.snakeyaml.Yaml 23 | 24 | import scala.collection.JavaConverters._ 25 | 26 | object GitHubActionsPlugin extends AutoPlugin { 27 | 28 | override def requires = plugins.JvmPlugin 29 | override def trigger = allRequirements 30 | 31 | object autoImport extends GitHubActionsKeys 32 | 33 | import autoImport._ 34 | 35 | private[this] def recursivelyConvert(a: Any): Any = a match { 36 | case map: java.util.Map[_, _] => 37 | map.asScala.toMap map { case (k, v) => k -> recursivelyConvert(v) } 38 | 39 | case ls: java.util.List[_] => 40 | ls.asScala.toList.map(recursivelyConvert) 41 | 42 | case i: java.lang.Integer => 43 | i.intValue 44 | 45 | case b: java.lang.Boolean => 46 | b.booleanValue 47 | 48 | case f: java.lang.Float => 49 | f.floatValue 50 | 51 | case d: java.lang.Double => 52 | d.doubleValue 53 | 54 | case l: java.lang.Long => 55 | l.longValue 56 | 57 | case s: String => s 58 | case null => null 59 | } 60 | 61 | private val workflowParseSettings = { 62 | sys.env.get("GITHUB_WORKFLOW") match { 63 | case Some(workflowName) => 64 | Seq( 65 | githubWorkflowName := workflowName, 66 | 67 | githubWorkflowDefinition := { 68 | val log = sLog.value 69 | val name = githubWorkflowName.value 70 | val base = baseDirectory.value 71 | 72 | if (name != null) { 73 | val workflowsDir = base / ".github" / "workflows" 74 | 75 | if (workflowsDir.exists() && workflowsDir.isDirectory()) { 76 | log.info(s"looking for workflow definition in $workflowsDir") 77 | 78 | val results = workflowsDir.listFiles().filter(_.getName.endsWith(".yml")).toList.view flatMap { potential => 79 | Using.fileInputStream(potential) { fis => 80 | Option(new Yaml().load[Any](fis)) collect { 81 | case map: java.util.Map[_, _] => 82 | map.asScala.toMap map { case (k, v) => k.toString -> recursivelyConvert(v) } 83 | } 84 | } 85 | } filter ( _ get "name" match { 86 | case Some(nameValue) => 87 | nameValue == name 88 | case None => 89 | log.warn("GitHub action yml file does not contain 'name' key") 90 | false 91 | }) 92 | 93 | results.headOption getOrElse { 94 | log.warn("unable to find or parse workflow YAML definition") 95 | log.warn("assuming the empty map for `githubWorkflowDefinition`") 96 | 97 | Map() 98 | } 99 | } else { 100 | Map() // silently pretend nothing is wrong, because we're probably running in a meta-plugin or something random 101 | } 102 | } else { 103 | log.warn("sbt does not appear to be running within GitHub Actions ($GITHUB_WORKFLOW is undefined)") 104 | log.warn("assuming the empty map for `githubWorkflowDefinition`") 105 | 106 | Map() 107 | } 108 | }) 109 | 110 | case None => 111 | Seq() 112 | } 113 | } 114 | 115 | override def buildSettings = workflowParseSettings 116 | 117 | override def globalSettings = Seq( 118 | githubIsWorkflowBuild := sys.env.get("GITHUB_ACTIONS").map("true" == _).getOrElse(false)) 119 | } 120 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/JavaSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | final case class JavaSpec(dist: JavaSpec.Distribution, version: String) { 20 | def render: String = dist match { 21 | case JavaSpec.Distribution.GraalVM(Graalvm.Version(gversion)) => 22 | s"graal_$gversion@$version" 23 | case JavaSpec.Distribution.GraalVM(Graalvm.Distribution(distribution)) => 24 | s"graal_$distribution@$version" 25 | case dist => s"${dist.rendering}@$version" 26 | } 27 | } 28 | 29 | /** 30 | * @see https://github.com/graalvm/setup-graalvm#migrating-from-graalvm-223-or-earlier-to-the-new-graalvm-for-jdk-17-and-later 31 | */ 32 | sealed trait Graalvm extends Product with Serializable { 33 | private[sbtghactions] def compile: String 34 | } 35 | 36 | object Graalvm { 37 | /** 38 | * For versions of Graalvm JDK 17 or earlier 39 | */ 40 | final case class Version(version: String) extends Graalvm { 41 | override private[sbtghactions] val compile: String = version 42 | } 43 | 44 | /** 45 | * For versions of Graalvm JDK 17 or later. Currently valid distributions are 46 | * graalvm, graalvm-community or mandrel 47 | */ 48 | final case class Distribution(distribution: String) extends Graalvm { 49 | override private[sbtghactions] val compile: String = distribution 50 | } 51 | } 52 | 53 | object JavaSpec { 54 | 55 | def temurin(version: String): JavaSpec = JavaSpec(Distribution.Temurin, version) 56 | 57 | def zulu(version: String): JavaSpec = JavaSpec(Distribution.Zulu, version) 58 | 59 | def corretto(version: String): JavaSpec = JavaSpec(Distribution.Corretto, version) 60 | 61 | private[sbtghactions] object JavaVersionExtractor { 62 | def unapply(version: String): Option[Int] = 63 | version.split("\\.").headOption.map(_.toInt) 64 | } 65 | 66 | def graalvm(graal: Graalvm, version: String): JavaSpec = { 67 | (graal, version) match { 68 | case (Graalvm.Version(_), JavaVersionExtractor(javaVersion)) if javaVersion > 17 => 69 | throw new IllegalArgumentException("Please use Graalvm.Distribution for JDK's newer than 17") 70 | case (Graalvm.Distribution(_), JavaVersionExtractor(javaVersion)) if javaVersion < 17 => 71 | throw new IllegalArgumentException("Graalvm.Distribution is not compatible with JDK's older than 17") 72 | case _ => 73 | } 74 | 75 | JavaSpec(Distribution.GraalVM(graal), version) 76 | } 77 | 78 | @deprecated("Use graalvm(graal: Graalvm, version: String) instead", "0.17.0") 79 | def graalvm(graal: String, version: String): JavaSpec = 80 | graalvm(Graalvm.Version(graal), version: String) 81 | 82 | sealed abstract class Distribution(val rendering: String) extends Product with Serializable 83 | 84 | object Distribution { 85 | case object Temurin extends Distribution("temurin") 86 | case object Zulu extends Distribution("zulu") 87 | case object Adopt extends Distribution("adopt-hotspot") 88 | case object OpenJ9 extends Distribution("adopt-openj9") 89 | case object Liberica extends Distribution("liberica") 90 | case object Corretto extends Distribution("corretto") 91 | final case class GraalVM(graalvm: Graalvm) extends Distribution(graalvm.compile) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/JobContainer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | final case class JobContainer( 20 | image: String, 21 | credentials: Option[(String, String)] = None, 22 | env: Map[String, String] = Map(), 23 | volumes: Map[String, String] = Map(), 24 | ports: List[Int] = Nil, 25 | options: List[String] = Nil) 26 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/JobEnvironment.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import java.net.URL 20 | 21 | final case class JobEnvironment(name: String, url: Option[URL] = None) 22 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/PREventType.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | sealed trait PREventType extends Product with Serializable 20 | 21 | object PREventType { 22 | val Defaults = List(Opened, Reopened, Synchronize) 23 | 24 | case object Assigned extends PREventType 25 | case object Unassigned extends PREventType 26 | case object Labeled extends PREventType 27 | case object Unlabeled extends PREventType 28 | case object Opened extends PREventType 29 | case object Edited extends PREventType 30 | case object Closed extends PREventType 31 | case object Reopened extends PREventType 32 | case object Synchronize extends PREventType 33 | case object ReadyForReview extends PREventType 34 | case object Locked extends PREventType 35 | case object Unlocked extends PREventType 36 | case object ReviewRequested extends PREventType 37 | case object ReviewRequestRemoved extends PREventType 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/Paths.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | sealed trait Paths extends Product with Serializable 20 | 21 | object Paths { 22 | final case class Include(paths: List[String]) extends Paths 23 | 24 | final case class Ignore(path: List[String]) extends Paths 25 | 26 | case object None extends Paths 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/PermissionScope.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | sealed trait Permissions extends Product with Serializable 20 | 21 | /** 22 | * @see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs#overview 23 | */ 24 | object Permissions { 25 | case object ReadAll extends Permissions 26 | case object WriteAll extends Permissions 27 | case object None extends Permissions 28 | final case class Specify(values: Map[PermissionScope, PermissionValue]) extends Permissions 29 | } 30 | 31 | sealed trait PermissionScope extends Product with Serializable 32 | 33 | object PermissionScope { 34 | case object Actions extends PermissionScope 35 | case object Attestations extends PermissionScope 36 | case object Checks extends PermissionScope 37 | case object Contents extends PermissionScope 38 | case object Deployments extends PermissionScope 39 | case object IdToken extends PermissionScope 40 | case object Issues extends PermissionScope 41 | case object Discussions extends PermissionScope 42 | case object Packages extends PermissionScope 43 | case object Pages extends PermissionScope 44 | case object PullRequests extends PermissionScope 45 | case object RepositoryProjects extends PermissionScope 46 | case object SecurityEvents extends PermissionScope 47 | case object Statuses extends PermissionScope 48 | } 49 | 50 | sealed trait PermissionValue extends Product with Serializable 51 | 52 | object PermissionValue { 53 | case object Read extends PermissionValue 54 | case object Write extends PermissionValue 55 | case object None extends PermissionValue 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/Ref.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | sealed trait Ref extends Product with Serializable 20 | 21 | object Ref { 22 | final case class Branch(name: String) extends Ref 23 | final case class Tag(name: String) extends Ref 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/RefPredicate.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | sealed trait RefPredicate extends Product with Serializable 20 | 21 | object RefPredicate { 22 | final case class Equals(ref: Ref) extends RefPredicate 23 | final case class Contains(ref: Ref) extends RefPredicate 24 | final case class StartsWith(ref: Ref) extends RefPredicate 25 | final case class EndsWith(ref: Ref) extends RefPredicate 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/UseRef.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | sealed trait UseRef extends Product with Serializable 20 | 21 | object UseRef { 22 | final case class Public(owner: String, repo: String, ref: String) extends UseRef 23 | final case class Local(path: String) extends UseRef 24 | final case class Docker(image: String, tag: String, host: Option[String] = None) extends UseRef 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/WorkflowJob.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import scala.concurrent.duration.FiniteDuration 20 | 21 | final case class WorkflowJob( 22 | id: String, 23 | name: String, 24 | steps: List[WorkflowStep], 25 | sbtStepPreamble: List[String] = List(), 26 | cond: Option[String] = None, 27 | permissions: Option[Permissions] = None, 28 | env: Map[String, String] = Map(), 29 | oses: List[String] = List("ubuntu-latest"), 30 | scalas: List[String] = List("2.13.10"), 31 | javas: List[JavaSpec] = List(JavaSpec.zulu("8")), 32 | needs: List[String] = List(), 33 | matrixFailFast: Option[Boolean] = None, 34 | matrixAdds: Map[String, List[String]] = Map(), 35 | matrixIncs: List[MatrixInclude] = List(), 36 | matrixExcs: List[MatrixExclude] = List(), 37 | runsOnExtraLabels: List[String] = List(), 38 | container: Option[JobContainer] = None, 39 | environment: Option[JobEnvironment] = None, 40 | timeout: Option[FiniteDuration] = None) 41 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/WorkflowStep.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import scala.collection.immutable.ListMap 20 | 21 | import scala.concurrent.duration.FiniteDuration 22 | 23 | sealed trait WorkflowStep extends Product with Serializable { 24 | def id: Option[String] 25 | def name: Option[String] 26 | def cond: Option[String] 27 | def env: Map[String, String] 28 | def timeout: Option[FiniteDuration] 29 | } 30 | 31 | object WorkflowStep { 32 | 33 | val DefaultSbtStepPreamble: List[String] = List(s"++ $${{ matrix.scala }}") 34 | 35 | val CheckoutFull: WorkflowStep = Use( 36 | UseRef.Public("actions", "checkout", "v4"), 37 | name = Some("Checkout current branch (full)"), 38 | params = Map("fetch-depth" -> "0")) 39 | 40 | val Checkout: WorkflowStep = Use(UseRef.Public("actions", "checkout", "v4"), name = Some("Checkout current branch (fast)")) 41 | 42 | def SetupJava(versions: List[JavaSpec]): List[WorkflowStep] = 43 | versions map { 44 | case jv @ JavaSpec(JavaSpec.Distribution.GraalVM(Graalvm.Version(graalVersion)), version) => 45 | WorkflowStep.Use( 46 | UseRef.Public("graalvm", "setup-graalvm", "v1"), 47 | name = Some(s"Setup GraalVM (${jv.render})"), 48 | cond = Some(s"matrix.java == '${jv.render}'"), 49 | params = ListMap( 50 | "version" -> graalVersion, 51 | "java-version" -> s"$version", 52 | "components" -> "native-image", 53 | "github-token" -> s"$${{ secrets.GITHUB_TOKEN }}", 54 | "cache" -> "sbt")) 55 | case jv @ JavaSpec(JavaSpec.Distribution.GraalVM(Graalvm.Distribution(distribution)), version) => 56 | WorkflowStep.Use( 57 | UseRef.Public("graalvm", "setup-graalvm", "v1"), 58 | name = Some(s"Setup GraalVM (${jv.render})"), 59 | cond = Some(s"matrix.java == '${jv.render}'"), 60 | params = ListMap( 61 | "java-version" -> s"$version", 62 | "distribution" -> distribution, 63 | "components" -> "native-image", 64 | "github-token" -> s"$${{ secrets.GITHUB_TOKEN }}", 65 | "cache" -> "sbt")) 66 | case jv @ JavaSpec(dist, version) => 67 | WorkflowStep.Use( 68 | UseRef.Public("actions", "setup-java", "v4"), 69 | name = Some(s"Setup Java (${jv.render})"), 70 | cond = Some(s"matrix.java == '${jv.render}'"), 71 | params = ListMap( 72 | "distribution" -> dist.rendering, 73 | "java-version" -> version, 74 | "cache" -> "sbt")) 75 | } 76 | 77 | def SetupSbt(runnerVersion: Option[String] = None): WorkflowStep = 78 | Use( 79 | ref = UseRef.Public("sbt", "setup-sbt", "v1"), 80 | params = runnerVersion match { 81 | case Some(v) => Map("sbt-runner-version" -> v) 82 | case None => Map() 83 | }, 84 | name = Some("Setup sbt"), 85 | ) 86 | 87 | val Tmate: WorkflowStep = Use(UseRef.Public("mxschmitt", "action-tmate", "v2"), name = Some("Setup tmate session")) 88 | 89 | def ComputeVar(name: String, cmd: String): WorkflowStep = 90 | Run( 91 | List("echo \"" + name + "=$(" + cmd + ")\" >> $GITHUB_ENV"), 92 | name = Some(s"Export $name")) 93 | 94 | def ComputePrependPATH(cmd: String): WorkflowStep = 95 | Run( 96 | List("echo \"$(" + cmd + ")\" >> $GITHUB_PATH"), 97 | name = Some(s"Prepend $$PATH using $cmd")) 98 | 99 | final case class Run(commands: List[String], id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), params: Map[String, String] = Map(), timeout: Option[FiniteDuration] = None) extends WorkflowStep 100 | final case class Sbt(commands: List[String], id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), params: Map[String, String] = Map(), timeout: Option[FiniteDuration] = None) extends WorkflowStep 101 | final case class Use(ref: UseRef, params: Map[String, String] = Map(), id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), timeout: Option[FiniteDuration] = None) extends WorkflowStep 102 | } 103 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/matrix.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | final case class MatrixInclude( 20 | matching: Map[String, String], 21 | additions: Map[String, String]) 22 | 23 | final case class MatrixExclude(matching: Map[String, String]) 24 | -------------------------------------------------------------------------------- /src/main/scala/sbtghactions/windows/PagefileFix.scala: -------------------------------------------------------------------------------- 1 | package sbtghactions.windows 2 | 3 | case class PagefileFix(minSize: String, maxSize: String, diskRoot: Option[String] = Some("C:")) 4 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/check-and-regenerate/build.sbt: -------------------------------------------------------------------------------- 1 | import scala.concurrent.duration._ 2 | 3 | organization := "com.github.sbt" 4 | version := "0.0.1" 5 | 6 | ThisBuild / crossScalaVersions := Seq("2.13.10", "2.12.17") 7 | ThisBuild / scalaVersion := crossScalaVersions.value.head 8 | 9 | ThisBuild / githubWorkflowTargetTags += "v*" 10 | 11 | ThisBuild / githubWorkflowJavaVersions += JavaSpec.graalvm(Graalvm.Version("22.3.0"), "17") 12 | ThisBuild / githubWorkflowPublishTargetBranches += RefPredicate.Equals(Ref.Tag("test")) 13 | 14 | ThisBuild / githubWorkflowBuildMatrixAdditions += "test" -> List("this", "is") 15 | 16 | ThisBuild / githubWorkflowBuildMatrixInclusions += MatrixInclude( 17 | Map("test" -> "this"), 18 | Map("extra" -> "sparta")) 19 | 20 | ThisBuild / githubWorkflowBuildMatrixExclusions += 21 | MatrixExclude(Map("scala" -> "2.12.17", "test" -> "is")) 22 | 23 | ThisBuild / githubWorkflowBuild += WorkflowStep.Run(List("echo yo")) 24 | 25 | ThisBuild / githubWorkflowPublish := 26 | Seq( 27 | WorkflowStep.Sbt( 28 | commands = List("ci-release"), 29 | name = Some("Publish project"), 30 | ), 31 | WorkflowStep.Run(List("echo sup")), 32 | ) 33 | ThisBuild / githubWorkflowBuildTimeout := Some(2.hours) 34 | 35 | ThisBuild / githubWorkflowPublishTimeout := Some(1.hour) 36 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | tags: [v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | jobs: 21 | build: 22 | name: Build and Test 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | scala: [2.13.10, 2.12.17] 27 | java: [zulu@8, graal_22.3.0@17] 28 | test: [this, is] 29 | include: 30 | - test: this 31 | extra: sparta 32 | exclude: 33 | - scala: 2.12.17 34 | test: is 35 | runs-on: ${{ matrix.os }} 36 | timeout-minutes: 120 37 | 38 | steps: 39 | - name: Checkout current branch (full) 40 | uses: actions/checkout@v4 41 | with: 42 | fetch-depth: 0 43 | 44 | - name: Setup Java (zulu@8) 45 | if: matrix.java == 'zulu@8' 46 | uses: actions/setup-java@v4 47 | with: 48 | distribution: zulu 49 | java-version: 8 50 | cache: sbt 51 | 52 | - name: Setup GraalVM (graal_22.3.0@17) 53 | if: matrix.java == 'graal_22.3.0@17' 54 | uses: graalvm/setup-graalvm@v1 55 | with: 56 | version: 22.3.0 57 | java-version: 17 58 | components: native-image 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | cache: sbt 61 | 62 | - name: Setup sbt 63 | uses: sbt/setup-sbt@v1 64 | 65 | - name: Check that workflows are up to date 66 | run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck 67 | 68 | - name: Build project 69 | run: sbt '++ ${{ matrix.scala }}' test 70 | 71 | - run: echo yo 72 | 73 | - name: Compress target directories 74 | run: tar cf targets.tar target project/target 75 | 76 | - name: Upload target directories 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 80 | path: targets.tar 81 | 82 | publish: 83 | name: Publish Artifacts 84 | needs: [build] 85 | if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/tags/test') 86 | strategy: 87 | matrix: 88 | os: [ubuntu-latest] 89 | scala: [2.13.10] 90 | java: [zulu@8] 91 | runs-on: ${{ matrix.os }} 92 | timeout-minutes: 60 93 | 94 | steps: 95 | - name: Checkout current branch (full) 96 | uses: actions/checkout@v4 97 | with: 98 | fetch-depth: 0 99 | 100 | - name: Setup Java (zulu@8) 101 | if: matrix.java == 'zulu@8' 102 | uses: actions/setup-java@v4 103 | with: 104 | distribution: zulu 105 | java-version: 8 106 | cache: sbt 107 | 108 | - name: Setup GraalVM (graal_22.3.0@17) 109 | if: matrix.java == 'graal_22.3.0@17' 110 | uses: graalvm/setup-graalvm@v1 111 | with: 112 | version: 22.3.0 113 | java-version: 17 114 | components: native-image 115 | github-token: ${{ secrets.GITHUB_TOKEN }} 116 | cache: sbt 117 | 118 | - name: Setup sbt 119 | uses: sbt/setup-sbt@v1 120 | 121 | - name: Download target directories (2.13.10) 122 | uses: actions/download-artifact@v4 123 | with: 124 | name: target-${{ matrix.os }}-2.13.10-${{ matrix.java }} 125 | 126 | - name: Inflate target directories (2.13.10) 127 | run: | 128 | tar xf targets.tar 129 | rm targets.tar 130 | 131 | - name: Download target directories (2.12.17) 132 | uses: actions/download-artifact@v4 133 | with: 134 | name: target-${{ matrix.os }}-2.12.17-${{ matrix.java }} 135 | 136 | - name: Inflate target directories (2.12.17) 137 | run: | 138 | tar xf targets.tar 139 | rm targets.tar 140 | 141 | - name: Publish project 142 | run: sbt ci-release 143 | 144 | - run: echo sup 145 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/check-and-regenerate/expected-clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | shell: bash {0} 21 | run: | 22 | # Customize those three lines with your repository and credentials: 23 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 24 | 25 | # A shortcut to call GitHub API. 26 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 27 | 28 | # A temporary file which receives HTTP response headers. 29 | TMPFILE=$(mktemp) 30 | 31 | # An associative array, key: artifact name, value: number of artifacts of that name. 32 | declare -A ARTCOUNT 33 | 34 | # Process all artifacts on this repository, loop on returned "pages". 35 | URL=$REPO/actions/artifacts 36 | while [[ -n "$URL" ]]; do 37 | 38 | # Get current page, get response headers in a temporary file. 39 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 40 | 41 | # Get URL of next page. Will be empty if we are at the last page. 42 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 43 | rm -f $TMPFILE 44 | 45 | # Number of artifacts on this page: 46 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 47 | 48 | # Loop on all artifacts on this page. 49 | for ((i=0; $i < $COUNT; i++)); do 50 | 51 | # Get name of artifact and count instances of this name. 52 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 53 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 54 | 55 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 56 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 57 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 58 | ghapi -X DELETE $REPO/actions/artifacts/$id 59 | done 60 | done 61 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/check-and-regenerate/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | sys.props.get("plugin.version") match { 2 | case Some(x) => addSbtPlugin("com.github.sbt" % "sbt-github-actions" % x) 3 | case _ => sys.error("""|The system property 'plugin.version' is not defined. 4 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) 5 | } 6 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/check-and-regenerate/test: -------------------------------------------------------------------------------- 1 | -> githubWorkflowCheck 2 | > githubWorkflowGenerate 3 | > githubWorkflowCheck 4 | $ copy-file .github/workflows/clean.yml .github/workflows/ci.yml 5 | -> githubWorkflowCheck 6 | > githubWorkflowGenerate 7 | > githubWorkflowCheck 8 | $ copy-file .github/workflows/ci.yml .github/workflows/clean.yml 9 | -> githubWorkflowCheck 10 | > githubWorkflowGenerate 11 | > githubWorkflowCheck 12 | $ copy-file expected-ci.yml .github/workflows/ci.yml 13 | > githubWorkflowCheck 14 | $ copy-file expected-clean.yml .github/workflows/clean.yml 15 | > githubWorkflowCheck 16 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/githubworkflowoses-clean-publish/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | name: Build and Test 22 | strategy: 23 | matrix: 24 | os: [windows-latest] 25 | scala: [2.13.10, 2.12.17] 26 | java: [zulu@8] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Ignore line ending differences in git 30 | if: contains(runner.os, 'windows') 31 | shell: bash 32 | run: git config --global core.autocrlf false 33 | 34 | - name: Configure pagefile for Windows 35 | if: contains(runner.os, 'windows') 36 | uses: al-cheb/configure-pagefile-action@v1.4 37 | with: 38 | minimum-size: 2GB 39 | maximum-size: 8GB 40 | disk-root: 'C:' 41 | 42 | - name: Checkout current branch (full) 43 | uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 46 | 47 | - name: Setup Java (zulu@8) 48 | if: matrix.java == 'zulu@8' 49 | uses: actions/setup-java@v4 50 | with: 51 | distribution: zulu 52 | java-version: 8 53 | cache: sbt 54 | 55 | - name: Setup sbt 56 | uses: sbt/setup-sbt@v1 57 | 58 | - name: Check that workflows are up to date 59 | shell: bash 60 | run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck 61 | 62 | - name: Build project 63 | shell: bash 64 | run: sbt '++ ${{ matrix.scala }}' test 65 | 66 | - name: Compress target directories 67 | shell: bash 68 | run: tar cf targets.tar target project/target 69 | 70 | - name: Upload target directories 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 74 | path: targets.tar 75 | 76 | publish: 77 | name: Publish Artifacts 78 | needs: [build] 79 | if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main') 80 | strategy: 81 | matrix: 82 | os: [windows-latest] 83 | scala: [2.13.10] 84 | java: [zulu@8] 85 | runs-on: ${{ matrix.os }} 86 | steps: 87 | - name: Ignore line ending differences in git 88 | if: contains(runner.os, 'windows') 89 | shell: bash 90 | run: git config --global core.autocrlf false 91 | 92 | - name: Configure pagefile for Windows 93 | if: contains(runner.os, 'windows') 94 | uses: al-cheb/configure-pagefile-action@v1.4 95 | with: 96 | minimum-size: 2GB 97 | maximum-size: 8GB 98 | disk-root: 'C:' 99 | 100 | - name: Checkout current branch (full) 101 | uses: actions/checkout@v4 102 | with: 103 | fetch-depth: 0 104 | 105 | - name: Setup Java (zulu@8) 106 | if: matrix.java == 'zulu@8' 107 | uses: actions/setup-java@v4 108 | with: 109 | distribution: zulu 110 | java-version: 8 111 | cache: sbt 112 | 113 | - name: Setup sbt 114 | uses: sbt/setup-sbt@v1 115 | 116 | - name: Download target directories (2.13.10) 117 | uses: actions/download-artifact@v4 118 | with: 119 | name: target-${{ matrix.os }}-2.13.10-${{ matrix.java }} 120 | 121 | - name: Inflate target directories (2.13.10) 122 | shell: bash 123 | run: | 124 | tar xf targets.tar 125 | rm targets.tar 126 | 127 | - name: Download target directories (2.12.17) 128 | uses: actions/download-artifact@v4 129 | with: 130 | name: target-${{ matrix.os }}-2.12.17-${{ matrix.java }} 131 | 132 | - name: Inflate target directories (2.12.17) 133 | shell: bash 134 | run: | 135 | tar xf targets.tar 136 | rm targets.tar 137 | 138 | - name: Publish project 139 | shell: bash 140 | run: sbt +publish 141 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/githubworkflowoses-clean-publish/.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: windows-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | shell: bash {0} 21 | run: | 22 | # Customize those three lines with your repository and credentials: 23 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 24 | 25 | # A shortcut to call GitHub API. 26 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 27 | 28 | # A temporary file which receives HTTP response headers. 29 | TMPFILE=$(mktemp) 30 | 31 | # An associative array, key: artifact name, value: number of artifacts of that name. 32 | declare -A ARTCOUNT 33 | 34 | # Process all artifacts on this repository, loop on returned "pages". 35 | URL=$REPO/actions/artifacts 36 | while [[ -n "$URL" ]]; do 37 | 38 | # Get current page, get response headers in a temporary file. 39 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 40 | 41 | # Get URL of next page. Will be empty if we are at the last page. 42 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 43 | rm -f $TMPFILE 44 | 45 | # Number of artifacts on this page: 46 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 47 | 48 | # Loop on all artifacts on this page. 49 | for ((i=0; $i < $COUNT; i++)); do 50 | 51 | # Get name of artifact and count instances of this name. 52 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 53 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 54 | 55 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 56 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 57 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 58 | ghapi -X DELETE $REPO/actions/artifacts/$id 59 | done 60 | done 61 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/githubworkflowoses-clean-publish/build.sbt: -------------------------------------------------------------------------------- 1 | organization := "com.github.sbt" 2 | version := "0.0.1" 3 | 4 | ThisBuild / crossScalaVersions := Seq("2.13.10", "2.12.17") 5 | ThisBuild / scalaVersion := crossScalaVersions.value.head 6 | ThisBuild / githubWorkflowOSes := Seq("windows-latest") 7 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/githubworkflowoses-clean-publish/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | sys.props.get("plugin.version") match { 2 | case Some(x) => addSbtPlugin("com.github.sbt" % "sbt-github-actions" % x) 3 | case _ => sys.error("""|The system property 'plugin.version' is not defined. 4 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) 5 | } 6 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/githubworkflowoses-clean-publish/test: -------------------------------------------------------------------------------- 1 | > githubWorkflowCheck 2 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/no-clean/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | name: Build and Test 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | scala: [2.13.10, 2.12.17] 26 | java: [zulu@8] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout current branch (full) 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Java (zulu@8) 35 | if: matrix.java == 'zulu@8' 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: zulu 39 | java-version: 8 40 | cache: sbt 41 | 42 | - name: Setup sbt 43 | uses: sbt/setup-sbt@v1 44 | 45 | - name: Check that workflows are up to date 46 | run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck 47 | 48 | - name: Build project 49 | run: sbt '++ ${{ matrix.scala }}' test 50 | 51 | - name: Compress target directories 52 | run: tar cf targets.tar target project/target 53 | 54 | - name: Upload target directories 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 58 | path: targets.tar 59 | 60 | publish: 61 | name: Publish Artifacts 62 | needs: [build] 63 | if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main') 64 | strategy: 65 | matrix: 66 | os: [ubuntu-latest] 67 | scala: [2.13.10] 68 | java: [zulu@8] 69 | runs-on: ${{ matrix.os }} 70 | steps: 71 | - name: Checkout current branch (full) 72 | uses: actions/checkout@v4 73 | with: 74 | fetch-depth: 0 75 | 76 | - name: Setup Java (zulu@8) 77 | if: matrix.java == 'zulu@8' 78 | uses: actions/setup-java@v4 79 | with: 80 | distribution: zulu 81 | java-version: 8 82 | cache: sbt 83 | 84 | - name: Setup sbt 85 | uses: sbt/setup-sbt@v1 86 | 87 | - name: Download target directories (2.13.10) 88 | uses: actions/download-artifact@v4 89 | with: 90 | name: target-${{ matrix.os }}-2.13.10-${{ matrix.java }} 91 | 92 | - name: Inflate target directories (2.13.10) 93 | run: | 94 | tar xf targets.tar 95 | rm targets.tar 96 | 97 | - name: Download target directories (2.12.17) 98 | uses: actions/download-artifact@v4 99 | with: 100 | name: target-${{ matrix.os }}-2.12.17-${{ matrix.java }} 101 | 102 | - name: Inflate target directories (2.12.17) 103 | run: | 104 | tar xf targets.tar 105 | rm targets.tar 106 | 107 | - name: Publish project 108 | run: sbt +publish 109 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/no-clean/build.sbt: -------------------------------------------------------------------------------- 1 | organization := "com.github.sbt" 2 | version := "0.0.1" 3 | 4 | ThisBuild / crossScalaVersions := Seq("2.13.10", "2.12.17") 5 | ThisBuild / scalaVersion := crossScalaVersions.value.head 6 | ThisBuild / githubWorkflowIncludeClean := false 7 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/no-clean/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | sys.props.get("plugin.version") match { 2 | case Some(x) => addSbtPlugin("com.github.sbt" % "sbt-github-actions" % x) 3 | case _ => sys.error("""|The system property 'plugin.version' is not defined. 4 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) 5 | } 6 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/no-clean/test: -------------------------------------------------------------------------------- 1 | > githubWorkflowCheck 2 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/non-existent-target/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | name: Build and Test 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | scala: [2.13.10] 26 | java: [zulu@8] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout current branch (full) 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Java (zulu@8) 35 | if: matrix.java == 'zulu@8' 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: zulu 39 | java-version: 8 40 | cache: sbt 41 | 42 | - name: Setup sbt 43 | uses: sbt/setup-sbt@v1 44 | 45 | - name: Check that workflows are up to date 46 | run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck 47 | 48 | - run: sbt '++ ${{ matrix.scala }}' withTarget/compile 49 | 50 | - name: Compress target directories 51 | run: tar cf targets.tar target withTarget/target project/target 52 | 53 | - name: Upload target directories 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 57 | path: targets.tar 58 | 59 | publish: 60 | name: Publish Artifacts 61 | needs: [build] 62 | if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main') 63 | strategy: 64 | matrix: 65 | os: [ubuntu-latest] 66 | scala: [2.13.10] 67 | java: [zulu@8] 68 | runs-on: ${{ matrix.os }} 69 | steps: 70 | - name: Checkout current branch (full) 71 | uses: actions/checkout@v4 72 | with: 73 | fetch-depth: 0 74 | 75 | - name: Setup Java (zulu@8) 76 | if: matrix.java == 'zulu@8' 77 | uses: actions/setup-java@v4 78 | with: 79 | distribution: zulu 80 | java-version: 8 81 | cache: sbt 82 | 83 | - name: Setup sbt 84 | uses: sbt/setup-sbt@v1 85 | 86 | - name: Download target directories (2.13.10) 87 | uses: actions/download-artifact@v4 88 | with: 89 | name: target-${{ matrix.os }}-2.13.10-${{ matrix.java }} 90 | 91 | - name: Inflate target directories (2.13.10) 92 | run: | 93 | tar xf targets.tar 94 | rm targets.tar 95 | 96 | - name: Publish project 97 | run: sbt +publish 98 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/non-existent-target/.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | shell: bash {0} 21 | run: | 22 | # Customize those three lines with your repository and credentials: 23 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 24 | 25 | # A shortcut to call GitHub API. 26 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 27 | 28 | # A temporary file which receives HTTP response headers. 29 | TMPFILE=$(mktemp) 30 | 31 | # An associative array, key: artifact name, value: number of artifacts of that name. 32 | declare -A ARTCOUNT 33 | 34 | # Process all artifacts on this repository, loop on returned "pages". 35 | URL=$REPO/actions/artifacts 36 | while [[ -n "$URL" ]]; do 37 | 38 | # Get current page, get response headers in a temporary file. 39 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 40 | 41 | # Get URL of next page. Will be empty if we are at the last page. 42 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 43 | rm -f $TMPFILE 44 | 45 | # Number of artifacts on this page: 46 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 47 | 48 | # Loop on all artifacts on this page. 49 | for ((i=0; $i < $COUNT; i++)); do 50 | 51 | # Get name of artifact and count instances of this name. 52 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 53 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 54 | 55 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 56 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 57 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 58 | ghapi -X DELETE $REPO/actions/artifacts/$id 59 | done 60 | done 61 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/non-existent-target/build.sbt: -------------------------------------------------------------------------------- 1 | organization := "com.github.sbt" 2 | version := "0.0.1" 3 | 4 | ThisBuild / crossScalaVersions := Seq("2.13.10") 5 | ThisBuild / scalaVersion := crossScalaVersions.value.head 6 | 7 | // explicitly don't build `withoutTarget` 8 | ThisBuild / githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("withTarget/compile"))) 9 | 10 | lazy val root = project.in(file(".")).aggregate(withTarget, withoutTarget) 11 | 12 | lazy val withTarget = project.in(file("withTarget")) 13 | 14 | lazy val withoutTarget = project.in(file("withoutTarget")) 15 | .settings(githubWorkflowArtifactUpload := false) 16 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/non-existent-target/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | sys.props.get("plugin.version") match { 2 | case Some(x) => addSbtPlugin("com.github.sbt" % "sbt-github-actions" % x) 3 | case _ => sys.error("""|The system property 'plugin.version' is not defined. 4 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) 5 | } 6 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/non-existent-target/test: -------------------------------------------------------------------------------- 1 | > githubWorkflowCheck 2 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/sbt-native-thin-client/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | name: Build and Test 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | scala: [2.12.19] 26 | java: [zulu@8] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout current branch (full) 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Java (zulu@8) 35 | if: matrix.java == 'zulu@8' 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: zulu 39 | java-version: 8 40 | cache: sbt 41 | 42 | - name: Setup sbt 43 | uses: sbt/setup-sbt@v1 44 | 45 | - name: Check that workflows are up to date 46 | run: sbt --client '++ ${{ matrix.scala }}; githubWorkflowCheck' 47 | 48 | - name: Build project 49 | run: sbt --client '++ ${{ matrix.scala }}; test' 50 | 51 | - run: sbt --client '++ ${{ matrix.scala }}; lots' 52 | 53 | - run: sbt --client '++ ${{ matrix.scala }}; of' 54 | 55 | - run: sbt --client '++ ${{ matrix.scala }}; sbt' 56 | 57 | - run: sbt --client '++ ${{ matrix.scala }}; tasks' 58 | 59 | - run: sbt --client '++ ${{ matrix.scala }}; run' 60 | 61 | - run: sbt --client '++ ${{ matrix.scala }}; as; separate; steps' 62 | 63 | - run: sbt --client '++ ${{ matrix.scala }}; using' 64 | 65 | - run: sbt --client '++ ${{ matrix.scala }}; sbtn' 66 | 67 | - name: Compress target directories 68 | run: tar cf targets.tar target project/target 69 | 70 | - name: Upload target directories 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 74 | path: targets.tar 75 | 76 | publish: 77 | name: Publish Artifacts 78 | needs: [build] 79 | if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main') 80 | strategy: 81 | matrix: 82 | os: [ubuntu-latest] 83 | scala: [2.12.19] 84 | java: [zulu@8] 85 | runs-on: ${{ matrix.os }} 86 | steps: 87 | - name: Checkout current branch (full) 88 | uses: actions/checkout@v4 89 | with: 90 | fetch-depth: 0 91 | 92 | - name: Setup Java (zulu@8) 93 | if: matrix.java == 'zulu@8' 94 | uses: actions/setup-java@v4 95 | with: 96 | distribution: zulu 97 | java-version: 8 98 | cache: sbt 99 | 100 | - name: Setup sbt 101 | uses: sbt/setup-sbt@v1 102 | 103 | - name: Download target directories (2.12.19) 104 | uses: actions/download-artifact@v4 105 | with: 106 | name: target-${{ matrix.os }}-2.12.19-${{ matrix.java }} 107 | 108 | - name: Inflate target directories (2.12.19) 109 | run: | 110 | tar xf targets.tar 111 | rm targets.tar 112 | 113 | - name: Publish project 114 | run: sbt --client '+publish' 115 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/sbt-native-thin-client/.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | shell: bash {0} 21 | run: | 22 | # Customize those three lines with your repository and credentials: 23 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 24 | 25 | # A shortcut to call GitHub API. 26 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 27 | 28 | # A temporary file which receives HTTP response headers. 29 | TMPFILE=$(mktemp) 30 | 31 | # An associative array, key: artifact name, value: number of artifacts of that name. 32 | declare -A ARTCOUNT 33 | 34 | # Process all artifacts on this repository, loop on returned "pages". 35 | URL=$REPO/actions/artifacts 36 | while [[ -n "$URL" ]]; do 37 | 38 | # Get current page, get response headers in a temporary file. 39 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 40 | 41 | # Get URL of next page. Will be empty if we are at the last page. 42 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 43 | rm -f $TMPFILE 44 | 45 | # Number of artifacts on this page: 46 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 47 | 48 | # Loop on all artifacts on this page. 49 | for ((i=0; $i < $COUNT; i++)); do 50 | 51 | # Get name of artifact and count instances of this name. 52 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 53 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 54 | 55 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 56 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 57 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 58 | ghapi -X DELETE $REPO/actions/artifacts/$id 59 | done 60 | done 61 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/sbt-native-thin-client/build.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / githubWorkflowBuild ++= List( 2 | WorkflowStep.Sbt(List("lots")), 3 | WorkflowStep.Sbt(List("of")), 4 | WorkflowStep.Sbt(List("sbt")), 5 | WorkflowStep.Sbt(List("tasks")), 6 | WorkflowStep.Sbt(List("run")), 7 | WorkflowStep.Sbt(List("as", "separate", "steps")), 8 | WorkflowStep.Sbt(List("using")), 9 | WorkflowStep.Sbt(List("sbtn")) 10 | ) 11 | 12 | ThisBuild / githubWorkflowUseSbtThinClient := true 13 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/sbt-native-thin-client/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | sys.props.get("plugin.version") match { 2 | case Some(x) => addSbtPlugin("com.github.sbt" % "sbt-github-actions" % x) 3 | case _ => sys.error("""|The system property 'plugin.version' is not defined. 4 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) 5 | } 6 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/sbt-native-thin-client/test: -------------------------------------------------------------------------------- 1 | > githubWorkflowCheck 2 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/suppressed-scala-version/build.sbt: -------------------------------------------------------------------------------- 1 | organization := "com.github.sbt" 2 | version := "0.0.1" 3 | 4 | ThisBuild / crossScalaVersions := Seq("2.13.10", "2.12.17") 5 | ThisBuild / scalaVersion := crossScalaVersions.value.head 6 | 7 | ThisBuild / githubWorkflowScalaVersions -= "2.12.17" 8 | 9 | ThisBuild / githubWorkflowTargetTags += "v*" 10 | 11 | ThisBuild / githubWorkflowPublishTargetBranches += RefPredicate.Equals(Ref.Tag("test")) 12 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/suppressed-scala-version/expected-ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | tags: [v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | jobs: 21 | build: 22 | name: Build and Test 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | scala: [2.13.10] 27 | java: [zulu@8] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - name: Checkout current branch (full) 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Setup Java (zulu@8) 36 | if: matrix.java == 'zulu@8' 37 | uses: actions/setup-java@v4 38 | with: 39 | distribution: zulu 40 | java-version: 8 41 | cache: sbt 42 | 43 | - name: Setup sbt 44 | uses: sbt/setup-sbt@v1 45 | 46 | - name: Check that workflows are up to date 47 | run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck 48 | 49 | - name: Build project 50 | run: sbt '++ ${{ matrix.scala }}' test 51 | 52 | - name: Compress target directories 53 | run: tar cf targets.tar target project/target 54 | 55 | - name: Upload target directories 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 59 | path: targets.tar 60 | 61 | publish: 62 | name: Publish Artifacts 63 | needs: [build] 64 | if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/tags/test') 65 | strategy: 66 | matrix: 67 | os: [ubuntu-latest] 68 | scala: [2.13.10] 69 | java: [zulu@8] 70 | runs-on: ${{ matrix.os }} 71 | steps: 72 | - name: Checkout current branch (full) 73 | uses: actions/checkout@v4 74 | with: 75 | fetch-depth: 0 76 | 77 | - name: Setup Java (zulu@8) 78 | if: matrix.java == 'zulu@8' 79 | uses: actions/setup-java@v4 80 | with: 81 | distribution: zulu 82 | java-version: 8 83 | cache: sbt 84 | 85 | - name: Setup sbt 86 | uses: sbt/setup-sbt@v1 87 | 88 | - name: Download target directories (2.13.10) 89 | uses: actions/download-artifact@v4 90 | with: 91 | name: target-${{ matrix.os }}-2.13.10-${{ matrix.java }} 92 | 93 | - name: Inflate target directories (2.13.10) 94 | run: | 95 | tar xf targets.tar 96 | rm targets.tar 97 | 98 | - name: Publish project 99 | run: sbt +publish 100 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/suppressed-scala-version/expected-clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | shell: bash {0} 21 | run: | 22 | # Customize those three lines with your repository and credentials: 23 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 24 | 25 | # A shortcut to call GitHub API. 26 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 27 | 28 | # A temporary file which receives HTTP response headers. 29 | TMPFILE=$(mktemp) 30 | 31 | # An associative array, key: artifact name, value: number of artifacts of that name. 32 | declare -A ARTCOUNT 33 | 34 | # Process all artifacts on this repository, loop on returned "pages". 35 | URL=$REPO/actions/artifacts 36 | while [[ -n "$URL" ]]; do 37 | 38 | # Get current page, get response headers in a temporary file. 39 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 40 | 41 | # Get URL of next page. Will be empty if we are at the last page. 42 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 43 | rm -f $TMPFILE 44 | 45 | # Number of artifacts on this page: 46 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 47 | 48 | # Loop on all artifacts on this page. 49 | for ((i=0; $i < $COUNT; i++)); do 50 | 51 | # Get name of artifact and count instances of this name. 52 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 53 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 54 | 55 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 56 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 57 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 58 | ghapi -X DELETE $REPO/actions/artifacts/$id 59 | done 60 | done 61 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/suppressed-scala-version/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | sys.props.get("plugin.version") match { 2 | case Some(x) => addSbtPlugin("com.github.sbt" % "sbt-github-actions" % x) 3 | case _ => sys.error("""|The system property 'plugin.version' is not defined. 4 | |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) 5 | } 6 | -------------------------------------------------------------------------------- /src/sbt-test/sbtghactions/suppressed-scala-version/test: -------------------------------------------------------------------------------- 1 | > githubWorkflowGenerate 2 | > githubWorkflowCheck 3 | $ copy-file expected-ci.yml .github/workflows/ci.yml 4 | $ copy-file expected-clean.yml .github/workflows/clean.yml 5 | > githubWorkflowCheck 6 | -------------------------------------------------------------------------------- /src/test/scala/sbtghactions/GenerativePluginSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Daniel Spiewak 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sbtghactions 18 | 19 | import org.specs2.mutable.Specification 20 | 21 | import java.net.URL 22 | import scala.concurrent.duration.DurationInt 23 | 24 | class GenerativePluginSpec extends Specification { 25 | import GenerativePlugin._ 26 | 27 | val header = """# This file was automatically generated by sbt-github-actions using the 28 | # githubWorkflowGenerate task. You should add and commit this file to 29 | # your git repository. It goes without saying that you shouldn't edit 30 | # this file by hand! Instead, if you wish to make changes, you should 31 | # change your sbt build configuration to revise the workflow description 32 | # to meet your needs, then regenerate this file. 33 | """ 34 | 35 | "workflow compilation" should { 36 | "produce the appropriate skeleton around a zero-job workflow" in { 37 | val expected = header + s""" 38 | |name: test 39 | | 40 | |on: 41 | | pull_request: 42 | | branches: [main] 43 | | push: 44 | | branches: [main] 45 | | 46 | |jobs: 47 | |${" " * 2} 48 | |""".stripMargin 49 | 50 | compileWorkflow("test", List("main"), Nil, Paths.None, PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected 51 | } 52 | 53 | "produce the appropriate skeleton around a zero-job workflow with non-empty tags" in { 54 | val expected = header + s""" 55 | |name: test 56 | | 57 | |on: 58 | | pull_request: 59 | | branches: [main] 60 | | push: 61 | | branches: [main] 62 | | tags: [howdy] 63 | | 64 | |jobs: 65 | |${" " * 2} 66 | |""".stripMargin 67 | 68 | compileWorkflow("test", List("main"), List("howdy"), Paths.None, PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected 69 | } 70 | 71 | "respect non-default pr types" in { 72 | val expected = header + s""" 73 | |name: test 74 | | 75 | |on: 76 | | pull_request: 77 | | branches: [main] 78 | | types: [ready_for_review, review_requested, opened] 79 | | push: 80 | | branches: [main] 81 | | 82 | |jobs: 83 | |${" " * 2} 84 | |""".stripMargin 85 | 86 | compileWorkflow("test", List("main"), Nil, Paths.None, List(PREventType.ReadyForReview, PREventType.ReviewRequested, PREventType.Opened), None, Map(), Nil, "sbt") mustEqual expected 87 | } 88 | 89 | "compile a one-job workflow targeting multiple branch patterns with a environment variables" in { 90 | val expected = header + s""" 91 | |name: test2 92 | | 93 | |on: 94 | | pull_request: 95 | | branches: [main, backport/v*] 96 | | push: 97 | | branches: [main, backport/v*] 98 | | 99 | |permissions: 100 | | id-token: write 101 | | 102 | |env: 103 | | GITHUB_TOKEN: $${{ secrets.GITHUB_TOKEN }} 104 | | 105 | |jobs: 106 | | build: 107 | | name: Build and Test 108 | | strategy: 109 | | matrix: 110 | | os: [ubuntu-latest] 111 | | scala: [2.13.10] 112 | | java: [zulu@8] 113 | | runs-on: $${{ matrix.os }} 114 | | steps: 115 | | - run: echo Hello World 116 | |""".stripMargin 117 | 118 | compileWorkflow( 119 | "test2", 120 | List("main", "backport/v*"), 121 | Nil, 122 | Paths.None, 123 | PREventType.Defaults, 124 | Some(Permissions.Specify(Map( 125 | PermissionScope.IdToken -> PermissionValue.Write 126 | ))), 127 | Map( 128 | "GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), 129 | List( 130 | WorkflowJob( 131 | "build", 132 | "Build and Test", 133 | List(WorkflowStep.Run(List("echo Hello World"))))), 134 | "sbt") mustEqual expected 135 | } 136 | 137 | "compile a workflow with two jobs" in { 138 | val expected = header + s""" 139 | |name: test3 140 | | 141 | |on: 142 | | pull_request: 143 | | branches: [main] 144 | | push: 145 | | branches: [main] 146 | | 147 | |jobs: 148 | | build: 149 | | name: Build and Test 150 | | strategy: 151 | | matrix: 152 | | os: [ubuntu-latest] 153 | | scala: [2.13.10] 154 | | java: [zulu@8] 155 | | runs-on: $${{ matrix.os }} 156 | | steps: 157 | | - run: echo yikes 158 | | 159 | | what: 160 | | name: If we just didn't 161 | | strategy: 162 | | matrix: 163 | | os: [ubuntu-latest] 164 | | scala: [2.13.10] 165 | | java: [zulu@8] 166 | | runs-on: $${{ matrix.os }} 167 | | steps: 168 | | - run: whoami 169 | |""".stripMargin 170 | 171 | compileWorkflow( 172 | "test3", 173 | List("main"), 174 | Nil, 175 | Paths.None, 176 | PREventType.Defaults, 177 | None, 178 | Map(), 179 | List( 180 | WorkflowJob( 181 | "build", 182 | "Build and Test", 183 | List(WorkflowStep.Run(List("echo yikes")))), 184 | 185 | WorkflowJob( 186 | "what", 187 | "If we just didn't", 188 | List(WorkflowStep.Run(List("whoami"))))), 189 | "") mustEqual expected 190 | } 191 | 192 | "render a simple container image" in { 193 | val expected = header + s""" 194 | |name: test4 195 | | 196 | |on: 197 | | pull_request: 198 | | branches: [main] 199 | | push: 200 | | branches: [main] 201 | | 202 | |jobs: 203 | | build: 204 | | name: Build and Test 205 | | strategy: 206 | | matrix: 207 | | os: [ubuntu-latest] 208 | | scala: [2.13.10] 209 | | java: [zulu@8] 210 | | runs-on: $${{ matrix.os }} 211 | | container: 'not:real-thing' 212 | | steps: 213 | | - run: echo yikes 214 | |""".stripMargin 215 | 216 | compileWorkflow( 217 | "test4", 218 | List("main"), 219 | Nil, 220 | Paths.None, 221 | PREventType.Defaults, 222 | None, 223 | Map(), 224 | List( 225 | WorkflowJob( 226 | "build", 227 | "Build and Test", 228 | List(WorkflowStep.Run(List("echo yikes"))), 229 | container = Some( 230 | JobContainer("not:real-thing")))), 231 | "") mustEqual expected 232 | } 233 | 234 | "render a container with all the trimmings" in { 235 | val expected = header + s""" 236 | |name: test4 237 | | 238 | |on: 239 | | pull_request: 240 | | branches: [main] 241 | | push: 242 | | branches: [main] 243 | | 244 | |jobs: 245 | | build: 246 | | name: Build and Test 247 | | strategy: 248 | | matrix: 249 | | os: [ubuntu-latest] 250 | | scala: [2.13.10] 251 | | java: [zulu@8] 252 | | runs-on: $${{ matrix.os }} 253 | | container: 254 | | image: 'also:not-real' 255 | | credentials: 256 | | username: janedoe 257 | | password: myvoice 258 | | env: 259 | | VERSION: 1.0 260 | | PATH: /nope 261 | | volumes: ['/source:/dest/ination'] 262 | | ports: [80, 443] 263 | | options: '--cpus 1' 264 | | steps: 265 | | - run: echo yikes 266 | |""".stripMargin 267 | 268 | compileWorkflow( 269 | "test4", 270 | List("main"), 271 | Nil, 272 | Paths.None, 273 | PREventType.Defaults, 274 | None, 275 | Map(), 276 | List( 277 | WorkflowJob( 278 | "build", 279 | "Build and Test", 280 | List(WorkflowStep.Run(List("echo yikes"))), 281 | container = Some( 282 | JobContainer( 283 | "also:not-real", 284 | credentials = Some("janedoe" -> "myvoice"), 285 | env = Map("VERSION" -> "1.0", "PATH" -> "/nope"), 286 | volumes = Map("/source" -> "/dest/ination"), 287 | ports = List(80, 443), 288 | options = List("--cpus", "1"))))), 289 | "") mustEqual expected 290 | } 291 | 292 | "render included paths on pull_request and push" in { 293 | val expected = header + s""" 294 | |name: test 295 | | 296 | |on: 297 | | pull_request: 298 | | branches: [main] 299 | | paths: ['**.scala', '**.sbt'] 300 | | push: 301 | | branches: [main] 302 | | paths: ['**.scala', '**.sbt'] 303 | | 304 | |jobs: 305 | |${" " * 2} 306 | |""".stripMargin 307 | 308 | compileWorkflow("test", List("main"), Nil, Paths.Include(List("**.scala", "**.sbt")), PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected 309 | } 310 | 311 | "render ignored paths on pull_request and push" in { 312 | val expected = header + s""" 313 | |name: test 314 | | 315 | |on: 316 | | pull_request: 317 | | branches: [main] 318 | | paths-ignore: [docs/**] 319 | | push: 320 | | branches: [main] 321 | | paths-ignore: [docs/**] 322 | | 323 | |jobs: 324 | |${" " * 2} 325 | |""".stripMargin 326 | 327 | compileWorkflow("test", List("main"), Nil, Paths.Ignore(List("docs/**")), PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected 328 | } 329 | } 330 | 331 | "step compilation" should { 332 | import WorkflowStep._ 333 | 334 | "compile a simple run without a name" in { 335 | compileStep(Run(List("echo hi")), "") mustEqual "- run: echo hi" 336 | } 337 | 338 | "compile a simple run with an id" in { 339 | compileStep(Run(List("echo hi"), id = Some("bippy")), "") mustEqual "- id: bippy\n run: echo hi" 340 | } 341 | 342 | "compile a simple run with a name" in { 343 | compileStep( 344 | Run( 345 | List("echo hi"), 346 | name = Some("nomenclature")), 347 | "") mustEqual "- name: nomenclature\n run: echo hi" 348 | } 349 | 350 | "compile a simple run with a name declaring the shell" in { 351 | compileStep( 352 | Run( 353 | List("echo hi"), 354 | name = Some("nomenclature")), 355 | "", 356 | Nil, 357 | declareShell = true) mustEqual "- name: nomenclature\n shell: bash\n run: echo hi" 358 | } 359 | 360 | "omit shell declaration on Use step" in { 361 | compileStep( 362 | Use( 363 | UseRef.Public( 364 | "repo", 365 | "slug", 366 | "v0")), 367 | "", 368 | Nil, 369 | declareShell = true) mustEqual "- uses: repo/slug@v0" 370 | } 371 | 372 | "preserve wonky version in Use" in { 373 | compileStep(Use(UseRef.Public("hello", "world", "v4.0.0")), "", Nil, declareShell = true) mustEqual "- uses: hello/world@v4.0.0" 374 | } 375 | 376 | "drop Use version prefix on anything that doesn't start with a number" in { 377 | compileStep(Use(UseRef.Public("hello", "world", "main")), "", Nil, declareShell = true) mustEqual "- uses: hello/world@main" 378 | } 379 | 380 | "compile sbt using the command provided" in { 381 | compileStep( 382 | Sbt(List("show scalaVersion", "compile", "test")), 383 | "$SBT") mustEqual s"- run: $$SBT '++ $${{ matrix.scala }}' 'show scalaVersion' compile test" 384 | } 385 | 386 | "compile sbt without switch command" in { 387 | compileStep( 388 | Sbt(List("ci-release")), 389 | "$SBT", 390 | sbtStepPreamble = Nil) mustEqual s"- run: $$SBT ci-release" 391 | } 392 | 393 | "compile sbt with parameters" in { 394 | compileStep( 395 | Sbt(List("compile", "test"), params = Map("abc" -> "def", "cafe" -> "@42")), 396 | "$SBT") mustEqual s"""- run: $$SBT '++ $${{ matrix.scala }}' compile test 397 | | with: 398 | | abc: def 399 | | cafe: '@42'""".stripMargin 400 | } 401 | 402 | "compile use without parameters" in { 403 | "public" >> { 404 | compileStep( 405 | Use(UseRef.Public("olafurpg", "setup-scala", "v13")), 406 | "") mustEqual "- uses: olafurpg/setup-scala@v13" 407 | } 408 | 409 | "directory" >> { 410 | compileStep( 411 | Use(UseRef.Local("foo/bar")), 412 | "") mustEqual "- uses: ./foo/bar" 413 | } 414 | 415 | "directory (quantified)" >> { 416 | compileStep( 417 | Use(UseRef.Local("./foo/bar")), 418 | "") mustEqual "- uses: ./foo/bar" 419 | } 420 | 421 | "docker" >> { 422 | "docker hub" >> { 423 | compileStep( 424 | Use(UseRef.Docker("subarctic-merecat", "2.3.4")), 425 | "") mustEqual "- uses: docker://subarctic-merecat:2.3.4" 426 | } 427 | 428 | "hosted" >> { 429 | compileStep( 430 | Use(UseRef.Docker("alpine-donkey", "2.3.4", host = Some("host.nope"))), 431 | "") mustEqual "- uses: docker://host.nope/alpine-donkey:2.3.4" 432 | } 433 | } 434 | } 435 | 436 | "compile use with two parameters" in { 437 | compileStep( 438 | Use(UseRef.Public("olafurpg", "setup-scala", "v13"), params = Map("abc" -> "def", "cafe" -> "@42")), 439 | "") mustEqual "- uses: olafurpg/setup-scala@v13\n with:\n abc: def\n cafe: '@42'" 440 | } 441 | 442 | "compile use with two parameters and environment variables" in { 443 | compileStep( 444 | Use( 445 | UseRef.Public( 446 | "derp", 447 | "nope", 448 | "v0"), 449 | params = Map("teh" -> "schizzle", "think" -> "positive"), 450 | env = Map("hi" -> "there")), 451 | "") mustEqual "- env:\n hi: there\n uses: derp/nope@v0\n with:\n teh: schizzle\n think: positive" 452 | } 453 | 454 | "compile a run step with multiple commands" in { 455 | compileStep(Run(List("whoami", "echo yo")), "") mustEqual "- run: |\n whoami\n echo yo" 456 | } 457 | 458 | "compile a run step with a conditional" in { 459 | compileStep( 460 | Run(List("users"), cond = Some("true")), 461 | "") mustEqual "- if: true\n run: users" 462 | } 463 | 464 | "compile a run with parameters" in { 465 | compileStep( 466 | Run(List("echo foo"), params = Map("abc" -> "def", "cafe" -> "@42")), 467 | "") mustEqual """- run: echo foo 468 | | with: 469 | | abc: def 470 | | cafe: '@42'""".stripMargin 471 | } 472 | 473 | "compile a run step with a timeout" in { 474 | compileStep( 475 | Run(List("users"), timeout = Some(1.hour)), 476 | "") mustEqual "- timeout-minutes: 60\n run: users" 477 | } 478 | 479 | "compile a run with parameters with space" in { 480 | compileStep( 481 | Run(List("echo foo"), params = Map("abc space" -> "def", "cafe test 1" -> "@42")), 482 | "") mustEqual """- run: echo foo 483 | | with: 484 | | abc space: def 485 | | cafe test 1: '@42'""".stripMargin 486 | } 487 | 488 | "throw error run with environment variables with space" in { 489 | compileStep( 490 | Run(List("echo foo"), env = Map("abc space" -> "def", "cafe test 1" -> "@42")), 491 | "") must throwA[RuntimeException] 492 | } 493 | 494 | } 495 | 496 | "job compilation" should { 497 | "compile a simple job with two steps" in { 498 | val results = compileJob( 499 | WorkflowJob( 500 | "bippy", 501 | "Bippity Bop Around the Clock", 502 | List( 503 | WorkflowStep.Run(List("echo hello")), 504 | WorkflowStep.Checkout)), 505 | "") 506 | 507 | results mustEqual s"""bippy: 508 | name: Bippity Bop Around the Clock 509 | strategy: 510 | matrix: 511 | os: [ubuntu-latest] 512 | scala: [2.13.10] 513 | java: [zulu@8] 514 | runs-on: $${{ matrix.os }} 515 | steps: 516 | - run: echo hello 517 | 518 | - name: Checkout current branch (fast) 519 | uses: actions/checkout@v4""" 520 | } 521 | 522 | "compile a job with one step and three oses" in { 523 | val results = compileJob( 524 | WorkflowJob( 525 | "derp", 526 | "Derples", 527 | List( 528 | WorkflowStep.Run(List("echo hello"))), 529 | oses = List("ubuntu-latest", "windows-latest", "macos-latest")), 530 | "") 531 | 532 | results mustEqual s"""derp: 533 | name: Derples 534 | strategy: 535 | matrix: 536 | os: [ubuntu-latest, windows-latest, macos-latest] 537 | scala: [2.13.10] 538 | java: [zulu@8] 539 | runs-on: $${{ matrix.os }} 540 | steps: 541 | - shell: bash 542 | run: echo hello""" 543 | } 544 | 545 | "compile a job with java setup, two JVMs and two Scalas" in { 546 | val javas = List(JavaSpec.temurin("17"), JavaSpec.graalvm(Graalvm.Version("22.3.0"), "11")) 547 | 548 | val results = compileJob( 549 | WorkflowJob( 550 | "abc", 551 | "How to get to...", 552 | WorkflowStep.SetupJava(javas), 553 | scalas = List("2.12.17", "2.13.10"), 554 | javas = javas), 555 | "") 556 | 557 | results mustEqual s"""abc: 558 | name: How to get to... 559 | strategy: 560 | matrix: 561 | os: [ubuntu-latest] 562 | scala: [2.12.17, 2.13.10] 563 | java: [temurin@17, graal_22.3.0@11] 564 | runs-on: $${{ matrix.os }} 565 | steps: 566 | - name: Setup Java (temurin@17) 567 | if: matrix.java == 'temurin@17' 568 | uses: actions/setup-java@v4 569 | with: 570 | distribution: temurin 571 | java-version: 17 572 | cache: sbt 573 | 574 | - name: Setup GraalVM (graal_22.3.0@11) 575 | if: matrix.java == 'graal_22.3.0@11' 576 | uses: graalvm/setup-graalvm@v1 577 | with: 578 | version: 22.3.0 579 | java-version: 11 580 | components: native-image 581 | github-token: $${{ secrets.GITHUB_TOKEN }} 582 | cache: sbt""" 583 | } 584 | 585 | "compile a job with java setup using new Graalvm distribution scheme" in { 586 | val javas = List(JavaSpec.graalvm(Graalvm.Distribution("graalvm"), "17")) 587 | 588 | val results = compileJob( 589 | WorkflowJob( 590 | "abc", 591 | "How to get to...", 592 | WorkflowStep.SetupJava(javas), 593 | scalas = List("2.12.17", "2.13.10"), 594 | javas = javas), 595 | "") 596 | 597 | results mustEqual s"""abc: 598 | name: How to get to... 599 | strategy: 600 | matrix: 601 | os: [ubuntu-latest] 602 | scala: [2.12.17, 2.13.10] 603 | java: [graal_graalvm@17] 604 | runs-on: $${{ matrix.os }} 605 | steps: 606 | - name: Setup GraalVM (graal_graalvm@17) 607 | if: matrix.java == 'graal_graalvm@17' 608 | uses: graalvm/setup-graalvm@v1 609 | with: 610 | java-version: 17 611 | distribution: graalvm 612 | components: native-image 613 | github-token: $${{ secrets.GITHUB_TOKEN }} 614 | cache: sbt""" 615 | } 616 | 617 | "throw an exception when using Graalvm.Distribution for JDK's older than 17" in { 618 | JavaSpec.graalvm(Graalvm.Distribution("graalvm"), "11") must throwA[IllegalArgumentException] 619 | } 620 | 621 | "throw an exception when using Graalvm.Version for JDK's newer than 17" in { 622 | JavaSpec.graalvm(Graalvm.Version("22.3.0"), "20") must throwA[IllegalArgumentException] 623 | } 624 | 625 | "compile a job with environment variables, conditional, and needs with an sbt step" in { 626 | val results = compileJob( 627 | WorkflowJob( 628 | "nada", 629 | "Moooo", 630 | List( 631 | WorkflowStep.Sbt(List("+compile"))), 632 | sbtStepPreamble = WorkflowStep.DefaultSbtStepPreamble, 633 | env = Map("not" -> "now"), 634 | cond = Some("boy != girl"), 635 | needs = List("unmet")), 636 | "csbt") 637 | 638 | results mustEqual s"""nada: 639 | name: Moooo 640 | needs: [unmet] 641 | if: boy != girl 642 | strategy: 643 | matrix: 644 | os: [ubuntu-latest] 645 | scala: [2.13.10] 646 | java: [zulu@8] 647 | runs-on: $${{ matrix.os }} 648 | env: 649 | not: now 650 | steps: 651 | - run: csbt '++ $${{ matrix.scala }}' +compile""" 652 | } 653 | 654 | "compile a job with an environment" in { 655 | val results = compileJob( 656 | WorkflowJob( 657 | "publish", 658 | "Publish Release", 659 | List( 660 | WorkflowStep.Sbt(List("ci-release"))), 661 | environment = Some(JobEnvironment("release"))), 662 | "csbt") 663 | 664 | results mustEqual s"""publish: 665 | name: Publish Release 666 | strategy: 667 | matrix: 668 | os: [ubuntu-latest] 669 | scala: [2.13.10] 670 | java: [zulu@8] 671 | runs-on: $${{ matrix.os }} 672 | environment: release 673 | steps: 674 | - run: csbt ci-release""" 675 | } 676 | 677 | "compile a job with specific permissions" in { 678 | val results = compileJob( 679 | WorkflowJob( 680 | "publish", 681 | "Publish Release", 682 | List( 683 | WorkflowStep.Sbt(List("ci-release"))), 684 | permissions = Some( 685 | Permissions.Specify(Map( 686 | PermissionScope.IdToken -> PermissionValue.Write 687 | )) 688 | )), 689 | "csbt") 690 | 691 | results mustEqual s"""publish: 692 | name: Publish Release 693 | strategy: 694 | matrix: 695 | os: [ubuntu-latest] 696 | scala: [2.13.10] 697 | java: [zulu@8] 698 | runs-on: $${{ matrix.os }} 699 | permissions: 700 | id-token: write 701 | steps: 702 | - run: csbt ci-release""" 703 | } 704 | 705 | "compile a job with read-all permissions" in { 706 | val results = compileJob( 707 | WorkflowJob( 708 | "publish", 709 | "Publish Release", 710 | List( 711 | WorkflowStep.Sbt(List("ci-release"))), 712 | permissions = Some(Permissions.ReadAll) 713 | ), 714 | "csbt") 715 | 716 | results mustEqual s"""publish: 717 | name: Publish Release 718 | strategy: 719 | matrix: 720 | os: [ubuntu-latest] 721 | scala: [2.13.10] 722 | java: [zulu@8] 723 | runs-on: $${{ matrix.os }} 724 | permissions: read-all 725 | steps: 726 | - run: csbt ci-release""" 727 | } 728 | 729 | "compile a job with an environment containing a url" in { 730 | val results = compileJob( 731 | WorkflowJob( 732 | "publish", 733 | "Publish Release", 734 | List( 735 | WorkflowStep.Sbt(List("ci-release"))), 736 | environment = Some(JobEnvironment("release", Some(new URL("https://github.com"))))), 737 | "csbt") 738 | 739 | results mustEqual s"""publish: 740 | name: Publish Release 741 | strategy: 742 | matrix: 743 | os: [ubuntu-latest] 744 | scala: [2.13.10] 745 | java: [zulu@8] 746 | runs-on: $${{ matrix.os }} 747 | environment: 748 | name: release 749 | url: 'https://github.com' 750 | steps: 751 | - run: csbt ci-release""" 752 | } 753 | 754 | "compile a job with additional matrix components" in { 755 | val results = compileJob( 756 | WorkflowJob( 757 | "bippy", 758 | "Bippity Bop Around the Clock", 759 | List( 760 | WorkflowStep.Run(List("echo ${{ matrix.test }}")), 761 | WorkflowStep.Checkout), 762 | matrixAdds = Map("test" -> List("1", "2")), 763 | matrixFailFast = Some(true)), 764 | "") 765 | 766 | results mustEqual s"""bippy: 767 | name: Bippity Bop Around the Clock 768 | strategy: 769 | fail-fast: true 770 | matrix: 771 | os: [ubuntu-latest] 772 | scala: [2.13.10] 773 | java: [zulu@8] 774 | test: [1, 2] 775 | runs-on: $${{ matrix.os }} 776 | steps: 777 | - run: echo $${{ matrix.test }} 778 | 779 | - name: Checkout current branch (fast) 780 | uses: actions/checkout@v4""" 781 | } 782 | 783 | "compile a job with extra runs-on labels" in { 784 | compileJob( 785 | WorkflowJob( 786 | "job", 787 | "my-name", 788 | List( 789 | WorkflowStep.Run(List("echo hello"))), 790 | runsOnExtraLabels = List("runner-label", "runner-group"), 791 | ), "") mustEqual """job: 792 | name: my-name 793 | strategy: 794 | matrix: 795 | os: [ubuntu-latest] 796 | scala: [2.13.10] 797 | java: [zulu@8] 798 | runs-on: [ "${{ matrix.os }}", runner-label, runner-group ] 799 | steps: 800 | - run: echo hello""" 801 | } 802 | 803 | "compile a job with a timeout" in { 804 | val results = compileJob( 805 | WorkflowJob( 806 | "publish", 807 | "Publish Release", 808 | List( 809 | WorkflowStep.Sbt(List("ci-release"))), 810 | timeout = Some(1.hour)), 811 | "csbt") 812 | 813 | results mustEqual s"""publish: 814 | name: Publish Release 815 | strategy: 816 | matrix: 817 | os: [ubuntu-latest] 818 | scala: [2.13.10] 819 | java: [zulu@8] 820 | runs-on: $${{ matrix.os }} 821 | timeout-minutes: 60 822 | 823 | steps: 824 | - run: csbt ci-release""" 825 | } 826 | 827 | "produce an error when compiling a job with `include` key in matrix" in { 828 | compileJob( 829 | WorkflowJob( 830 | "bippy", 831 | "Bippity Bop Around the Clock", 832 | List(), 833 | matrixAdds = Map("include" -> List("1", "2"))), 834 | "") must throwA[RuntimeException] 835 | } 836 | 837 | "produce an error when compiling a job with `exclude` key in matrix" in { 838 | compileJob( 839 | WorkflowJob( 840 | "bippy", 841 | "Bippity Bop Around the Clock", 842 | List(), 843 | matrixAdds = Map("exclude" -> List("1", "2"))), 844 | "") must throwA[RuntimeException] 845 | } 846 | 847 | "compile a job with a simple matching inclusion" in { 848 | val results = compileJob( 849 | WorkflowJob( 850 | "bippy", 851 | "Bippity Bop Around the Clock", 852 | List( 853 | WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), 854 | matrixIncs = List( 855 | MatrixInclude( 856 | Map("scala" -> "2.13.10"), 857 | Map("foo" -> "bar")))), 858 | "") 859 | 860 | results mustEqual s"""bippy: 861 | name: Bippity Bop Around the Clock 862 | strategy: 863 | matrix: 864 | os: [ubuntu-latest] 865 | scala: [2.13.10] 866 | java: [zulu@8] 867 | include: 868 | - scala: 2.13.10 869 | foo: bar 870 | runs-on: $${{ matrix.os }} 871 | steps: 872 | - run: echo $${{ matrix.scala }}""" 873 | } 874 | 875 | "produce an error with a non-matching inclusion key" in { 876 | compileJob( 877 | WorkflowJob( 878 | "bippy", 879 | "Bippity Bop Around the Clock", 880 | List( 881 | WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), 882 | matrixIncs = List( 883 | MatrixInclude( 884 | Map("scalanot" -> "2.13.10"), 885 | Map("foo" -> "bar")))), 886 | "") must throwA[RuntimeException] 887 | } 888 | 889 | "produce an error with a non-matching inclusion value" in { 890 | compileJob( 891 | WorkflowJob( 892 | "bippy", 893 | "Bippity Bop Around the Clock", 894 | List( 895 | WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), 896 | matrixIncs = List( 897 | MatrixInclude( 898 | Map("scala" -> "0.12.1"), 899 | Map("foo" -> "bar")))), 900 | "") must throwA[RuntimeException] 901 | } 902 | 903 | "compile a job with a simple matching exclusion" in { 904 | val results = compileJob( 905 | WorkflowJob( 906 | "bippy", 907 | "Bippity Bop Around the Clock", 908 | List( 909 | WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), 910 | matrixExcs = List( 911 | MatrixExclude( 912 | Map("scala" -> "2.13.10")))), 913 | "") 914 | 915 | results mustEqual s"""bippy: 916 | name: Bippity Bop Around the Clock 917 | strategy: 918 | matrix: 919 | os: [ubuntu-latest] 920 | scala: [2.13.10] 921 | java: [zulu@8] 922 | exclude: 923 | - scala: 2.13.10 924 | runs-on: $${{ matrix.os }} 925 | steps: 926 | - run: echo $${{ matrix.scala }}""" 927 | } 928 | 929 | "produce an error with a non-matching exclusion key" in { 930 | compileJob( 931 | WorkflowJob( 932 | "bippy", 933 | "Bippity Bop Around the Clock", 934 | List( 935 | WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), 936 | matrixExcs = List( 937 | MatrixExclude( 938 | Map("scalanot" -> "2.13.10")))), 939 | "") must throwA[RuntimeException] 940 | } 941 | 942 | "produce an error with a non-matching exclusion value" in { 943 | compileJob( 944 | WorkflowJob( 945 | "bippy", 946 | "Bippity Bop Around the Clock", 947 | List( 948 | WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), 949 | matrixExcs = List( 950 | MatrixExclude( 951 | Map("scala" -> "0.12.1")))), 952 | "") must throwA[RuntimeException] 953 | } 954 | 955 | "allow a matching JVM exclusion" in { 956 | compileJob( 957 | WorkflowJob( 958 | "bippy", 959 | "Bippity Bop Around the Clock", 960 | List( 961 | WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), 962 | matrixExcs = List( 963 | MatrixExclude( 964 | Map("java" -> JavaSpec.zulu("8").render)))), 965 | "") must not(throwA[RuntimeException]) 966 | } 967 | 968 | "compile a job with a long list of scala versions" in { 969 | val results = compileJob( 970 | WorkflowJob( 971 | "bippy", 972 | "Bippity Bop Around the Clock", 973 | List( 974 | WorkflowStep.Run(List("echo hello")), 975 | WorkflowStep.Checkout), 976 | scalas = List("this", "is", "a", "lot", "of", "versions", "meant", "to", "overflow", "the", "bounds", "checking")), 977 | "") 978 | 979 | results mustEqual s"""bippy: 980 | name: Bippity Bop Around the Clock 981 | strategy: 982 | matrix: 983 | os: [ubuntu-latest] 984 | scala: 985 | - this 986 | - is 987 | - a 988 | - lot 989 | - of 990 | - versions 991 | - meant 992 | - to 993 | - overflow 994 | - the 995 | - bounds 996 | - checking 997 | java: [zulu@8] 998 | runs-on: $${{ matrix.os }} 999 | steps: 1000 | - run: echo hello 1001 | 1002 | - name: Checkout current branch (fast) 1003 | uses: actions/checkout@v4""" 1004 | } 1005 | } 1006 | 1007 | "predicate compilation" >> { 1008 | import RefPredicate._ 1009 | import Ref._ 1010 | 1011 | "equals" >> { 1012 | compileBranchPredicate("thingy", Equals(Branch("other"))) mustEqual "thingy == 'refs/heads/other'" 1013 | } 1014 | 1015 | "contains" >> { 1016 | compileBranchPredicate("thingy", Contains(Tag("other"))) mustEqual "(startsWith(thingy, 'refs/tags/') && contains(thingy, 'other'))" 1017 | } 1018 | 1019 | "startsWith" >> { 1020 | compileBranchPredicate("thingy", StartsWith(Branch("other"))) mustEqual "startsWith(thingy, 'refs/heads/other')" 1021 | } 1022 | 1023 | "endsWith" >> { 1024 | compileBranchPredicate("thingy", EndsWith(Branch("other"))) mustEqual "(startsWith(thingy, 'refs/heads/') && endsWith(thingy, 'other'))" 1025 | } 1026 | } 1027 | 1028 | "pr event type compilation" >> { 1029 | import PREventType._ 1030 | 1031 | "assigned" >> (compilePREventType(Assigned) mustEqual "assigned") 1032 | "unassigned" >> (compilePREventType(Unassigned) mustEqual "unassigned") 1033 | "labeled" >> (compilePREventType(Labeled) mustEqual "labeled") 1034 | "unlabeled" >> (compilePREventType(Unlabeled) mustEqual "unlabeled") 1035 | "opened" >> (compilePREventType(Opened) mustEqual "opened") 1036 | "edited" >> (compilePREventType(Edited) mustEqual "edited") 1037 | "closed" >> (compilePREventType(Closed) mustEqual "closed") 1038 | "reopened" >> (compilePREventType(Reopened) mustEqual "reopened") 1039 | "synchronize" >> (compilePREventType(Synchronize) mustEqual "synchronize") 1040 | "ready_for_review" >> (compilePREventType(ReadyForReview) mustEqual "ready_for_review") 1041 | "locked" >> (compilePREventType(Locked) mustEqual "locked") 1042 | "unlocked" >> (compilePREventType(Unlocked) mustEqual "unlocked") 1043 | "review_requested" >> (compilePREventType(ReviewRequested) mustEqual "review_requested") 1044 | "review_request_removed" >> (compilePREventType(ReviewRequestRemoved) mustEqual "review_request_removed") 1045 | } 1046 | 1047 | "diff" should { 1048 | "highlight the first different character" in { 1049 | val expected = 1050 | """abc 1051 | | 1052 | |def 1053 | | 1054 | |ghi""".stripMargin 1055 | val actual = 1056 | """abc 1057 | | 1058 | |df 1059 | | 1060 | |ghi""".stripMargin 1061 | val expectedDiff = 1062 | """abc 1063 | | 1064 | |df 1065 | | ^ (different character) 1066 | | 1067 | |ghi""".stripMargin 1068 | val actualDiff = GenerativePlugin.diff(expected, actual) 1069 | expectedDiff mustEqual actualDiff 1070 | } 1071 | "highlight the missing lines" in { 1072 | val expected = 1073 | """abc 1074 | |def 1075 | |ghi""".stripMargin 1076 | val actual = 1077 | """abc 1078 | |def""".stripMargin 1079 | val expectedDiff = 1080 | """abc 1081 | |def 1082 | | ^ (missing lines)""".stripMargin 1083 | val actualDiff = GenerativePlugin.diff(expected, actual) 1084 | expectedDiff mustEqual actualDiff 1085 | } 1086 | "highlight the additionl lines" in { 1087 | val expected = 1088 | """abc 1089 | |def 1090 | |ghi""".stripMargin 1091 | val actual = 1092 | """abc 1093 | |def 1094 | |ghi 1095 | |jkl""".stripMargin 1096 | val expectedDiff = 1097 | """abc 1098 | |def 1099 | |ghi 1100 | | ^ (additional lines) 1101 | |jkl""".stripMargin 1102 | val actualDiff = GenerativePlugin.diff(expected, actual) 1103 | expectedDiff mustEqual actualDiff 1104 | } 1105 | } 1106 | } 1107 | --------------------------------------------------------------------------------