diff --git a/.circleci/config.pkl b/.circleci/config.pkl new file mode 100644 index 0000000..cd503fd --- /dev/null +++ b/.circleci/config.pkl @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// +amends "../packages/pkl.impl.circleci/PklCI.pkl" + +jobs { + ["build"] { + docker { + new { image = "cimg/openjdk:17.0" } + } + steps { + "checkout" + new RunStep { + name = "Build" + command = "./gradlew build" + environment {} + } + new StoreTestResults { path = "build/test-results" } + new PersistToWorkspaceStep { + root = "." + paths { + "build" + } + } + } + } + ["release"] { + docker { + new { image = "maniator/gh:v2.40.1" } + } + steps { + "checkout" // for `git tag` querying + new AttachWorkspaceStep { at = "." } + new RunStep { + name = "Publish release on GitHub" + // language=bash + command = #""" + if [[ -d build/releases && -n "$(ls -A build/releases)" ]] + then + REPO="${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" + for dir in build/releases/* + do + if [[ -d "$dir" ]] + then + pkg=$(basename "$dir") + if gh release view "$pkg" ; then + echo "Package $pkg already published" + else + # TODO we can be kinder to GitHub by querying once for all releases. + echo -n "> Releasing $pkg at SHA1 ${CIRCLE_SHA1}..." + gh release create "$pkg" \ + --title "$pkg" \ + --target "${CIRCLE_SHA1}" \ + --repo "$REPO" \ + --notes "Release of $pkg" \ + "$dir"/* + echo "DONE" + fi + else + echo "> SKIPPING $dir; not a directory" + fi + done + else + echo "No new packages to release." + fi + """# + } + } + } +} + +prb { + jobs { + "build" + } +} + +main { + jobs { + "build" + new { + ["release"] { + requires { + "build" + } + context { + "pkl-github-release" + } + } + } + } +} diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..6088136 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,85 @@ +# Generated from CircleCI.pkl. DO NOT EDIT. +version: '2.1' +orbs: + pr-approval: apple/pr-approval@0.1.0 +jobs: + build: + steps: + - checkout + - run: + command: ./gradlew build + name: Build + environment: {} + - store_test_results: + path: build/test-results + - persist_to_workspace: + root: '.' + paths: + - build + docker: + - image: cimg/openjdk:17.0 + release: + steps: + - checkout + - attach_workspace: + at: '.' + - run: + command: |- + if [[ -d build/releases && -n "$(ls -A build/releases)" ]] + then + REPO="${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" + for dir in build/releases/* + do + if [[ -d "$dir" ]] + then + pkg=$(basename "$dir") + if gh release view "$pkg" ; then + echo "Package $pkg already published" + else + # TODO we can be kinder to GitHub by querying once for all releases. + echo -n "> Releasing $pkg at SHA1 ${CIRCLE_SHA1}..." + gh release create "$pkg" \ + --title "$pkg" \ + --target "${CIRCLE_SHA1}" \ + --repo "$REPO" \ + --notes "Release of $pkg" \ + "$dir"/* + echo "DONE" + fi + else + echo "> SKIPPING $dir; not a directory" + fi + done + else + echo "No new packages to release." + fi + name: Publish release on GitHub + docker: + - image: maniator/gh:v2.40.1 +workflows: + prb: + jobs: + - hold: + type: approval + - pr-approval/authenticate: + context: pkl-pr-approval + - build: + requires: + - hold + - pr-approval/authenticate + when: + matches: + value: << pipeline.git.branch >> + pattern: ^pull/\d+(/head)?$ + main: + jobs: + - build + - release: + requires: + - build + context: + - pkl-github-release + when: + equal: + - main + - << pipeline.git.branch >> diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edd133d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc new file mode 100644 index 0000000..3dee37a --- /dev/null +++ b/CODE_OF_CONDUCT.adoc @@ -0,0 +1,78 @@ +== Code of Conduct + +=== Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our +project and our community a harassment-free experience for everyone, +regardless of age, body size, disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +=== Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual +attention or advances +* Trolling, insulting/derogatory comments, and personal or political +attacks +* Public or private harassment +* Publishing others’ private information, such as a physical or +electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a +professional setting + +=== Our Responsibilities + +Project maintainers are responsible for clarifying the standards of +acceptable behavior and are expected to take appropriate and fair +corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, +or reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, or to ban +temporarily or permanently any contributor for other behaviors that they +deem inappropriate, threatening, offensive, or harmful. + +=== Scope + +This Code of Conduct applies within all project spaces, and it also +applies when an individual is representing the project or its community +in public spaces. Examples of representing a project or community +include using an official project e-mail address, posting via an +official social media account, or acting as an appointed representative +at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +=== Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may +be reported by contacting the open source team at +opensource-conduct@group.apple.com. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. The project team is obligated to +maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted +separately. + +Project maintainers who do not follow or enforce the Code of Conduct in +good faith may face temporary or permanent repercussions as determined +by other members of the project’s leadership. + +=== Attribution + +This Code of Conduct is adapted from the +https://www.contributor-covenant.org[Contributor Covenant], version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 0000000..41b8f3b --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,44 @@ +:uri-github-issue-pkl: https://github.com/apple/pkl-pantry/issues/new +:uri-seven-rules: https://cbea.ms/git-commit/#seven-rules + += Pkl Project Commons Contributors Guide + +Welcome to the Pkl Project Commons community, and thank you for contributing! +This guide explains how to get involved. + +* <> +* <> +* <> + +== Licensing + +Pkl Project Commons is released under the Apache 2.0 license. +This is why we require that, by submitting a pull request, you acknowledge that you have the right to license your contribution to Apple and the community, and agree that your contribution is licensed under the Apache 2.0 license. + +== Issue Tracking + +To file a bug or feature request, use {uri-github-issue-pkl}[GitHub]. +Be sure to include the following information: + +* Context +** What are/were you trying to achieve? +** What's the impact of this bug/feature? + +== Pull Requests + +When preparing a pull request, follow this checklist: + +* Imitate the conventions of surrounding code. +* Format `.pkl` files in your PR with the JetBrains IDE formatter. +* Follow the {uri-seven-rules}[seven rules] of great Git commit messages: +** Separate subject from body with a blank line. +** Limit the subject line to 50 characters. +** Capitalize the subject line. +** Do not end the subject line with a period. +** Use the imperative mood in the subject line. +** Wrap the body at 72 characters. +** Use the body to explain what and why vs. how. + +== Maintainers + +The project’s maintainers (those with write access to the upstream repository) are listed in link:MAINTAINERS.adoc[]. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS.adoc b/MAINTAINERS.adoc new file mode 100644 index 0000000..b79b8d2 --- /dev/null +++ b/MAINTAINERS.adoc @@ -0,0 +1,11 @@ += MAINTAINERS + +This page lists all active Maintainers of this repository. + +See link:CONTRIBUTING.adoc[] for general contribution guidelines. + +== Maintainers (in alphabetical order) + +* https://github.com/bioball[Daniel Chao] +* https://github.com/stackoverflow[Islon Scherer] +* https://github.com/holzensp[Philip Hölzenspies] diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..eb66b11 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,17 @@ +Copyright © 2024 Apple Inc. and the Pkl project authors + +Portions of this software includes code from "Gradle" by Gradle, Inc. + +Copyright 2015 the original author or authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..317d101 --- /dev/null +++ b/README.adoc @@ -0,0 +1,5 @@ += Pkl Project Commons + +This repository houses libraries that are implementation details for the Pkl project. + +These libraries are not meant for general use. diff --git a/SECURITY.adoc b/SECURITY.adoc new file mode 100644 index 0000000..45f7d7d --- /dev/null +++ b/SECURITY.adoc @@ -0,0 +1,13 @@ += Security + +For the protection of our community, the Pkl team does not disclose, discuss, or confirm security issues until our investigation is complete and any necessary updates are generally available. + +== Reporting a security vulnerability + +If you have discovered a security vulnerability within the Pkl Project Commons project, please report it to us. +We welcome reports from everyone, including security researchers, developers, and users. + +Security vulnerabilities may be reported on the link:https://security.apple.com/submit[Report a vulnerability] form. +When submitting a vulnerability, select "Apple Devices and Software" as the affected platform, and "Open Source" as the affected area. + +For more information, see https://pkl-lang.org/security.html. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..9a8d624 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,178 @@ +import org.pkl.core.Version +import java.io.OutputStream +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import javax.net.ssl.HttpsURLConnection +import kotlin.io.path.isDirectory +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.log10 + +plugins { + kotlin("jvm").version(libs.versions.kotlin) + alias(libs.plugins.pkl) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + licenseHeader(""" + /** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + """.trimIndent()) + } + format("pkl") { + licenseHeader(""" + //===----------------------------------------------------------------------===// + // Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + // + // Licensed under the Apache License, Version 2.0 (the "License"); + // you may not use this file except in compliance with the License. + // You may obtain a copy of the License at + // + // https://www.apache.org/licenses/LICENSE-2.0 + // + // Unless required by applicable law or agreed to in writing, software + // distributed under the License is distributed on an "AS IS" BASIS, + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + // See the License for the specific language governing permissions and + // limitations under the License. + //===----------------------------------------------------------------------===// + """.trimIndent(), "(/// |/\\*\\*|module |import |amends |(\\w+))") + target("**/*.pkl", "**/PklProject") + } +} + +kotlin { + jvmToolchain(17) +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(libs.pklCore) + testImplementation(libs.junitEngine) + testImplementation(libs.junitParams) +} + +val repositoryUrl = "https://github.com/apple/pkl-pantry" + +val repositoryApiUrl = repositoryUrl.replace(Regex("github.com/"), "api.github.com/repos/") + +val projectDirs: List = + Files.list(Path.of("packages")) + .filter { it.isDirectory() } + .map { it.toFile() } + .toList() + +val outputDir = layout.buildDirectory + +pkl { + project { + resolvers { + register("resolveProjects") { + projectDirectories.from(projectDirs) + } + } + packagers { + register("createPackages") { + projectDirectories.from(projectDirs) + outputPath.set(outputDir.dir("generated/packages/%{name}/%{version}")) + junitReportsDir.set(outputDir.dir("test-results")) + } + } + } +} + +val resolveProjects = tasks.named("resolveProjects") { + group = "build" +} + +val createPackages = tasks.named("createPackages") { + group = "build" + dependsOn.add(resolveProjects) +} + +val isInCircleCi = System.getenv("CIRCLE_PROJECT_REPONAME") != null + +val prepareCiGit by tasks.registering { + enabled = isInCircleCi + exec { + commandLine("git", "config", "user.email", "pkl-oss@groups.apple.com") + } + exec { + commandLine("git", "config", "user.name", "The Pkl Team (automation)") + } +} + +val prepareReleases by tasks.registering { + group = "build" + dependsOn(createPackages, prepareCiGit) + inputs.files(projectDirs) + + doLast { + val releaseDir = file(outputDir.dir("releases")) + releaseDir.deleteRecursively() + val count = projectDirs.count() + val fmt = "%${max(1,ceil(log10(count.toDouble())).toInt())}d" + + for (i in projectDirs.indices) { + val dir = projectDirs[i] + print(" [${fmt.format(i + 1)}/$count] $dir: ") + val allVersions = file(outputDir.dir("generated/packages/${dir.name}")).list() + if (allVersions == null) { + println("∅") + continue + } + val latestVersion = allVersions.map(Version::parse).sortedWith(Version.comparator()).last() + val pkg = "${dir.name}@$latestVersion" + print("$pkg: ") + val conn = URI("${repositoryUrl}/releases/tag/${dir.name}@$latestVersion") + .toURL() + .openConnection() as HttpsURLConnection + if (conn.responseCode == 200) { + println("⏩") + continue + } + val taskOutput = StringBuilder() + exec { + commandLine("git", "tag", "-l", pkg) + logging.addStandardOutputListener { taskOutput.append(it) } + standardOutput = OutputStream.nullOutputStream() + } + if (taskOutput.contains(pkg)) { + println("☑️") + continue + } + for (artifact in file(outputDir.dir("generated/packages/${dir.name}/$latestVersion")).listFiles()!!) { + artifact.copyTo(releaseDir.resolve("$pkg/${artifact.name}"), true) + } + println("✅") + } + } +} + +tasks.test { + useJUnitPlatform() + dependsOn(createPackages) +} + +tasks.build { + dependsOn(prepareReleases) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..18f452c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..545eee4 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,14 @@ +[versions] +pkl = "0.25.1" +kotlin = "1.9.0" +junit = "5.10.0" +spotless = "6.25.0" + +[libraries] +junitEngine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } +junitParams = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" } +pklCore = { group = "org.pkl-lang", name = "pkl-core", version.ref = "pkl" } + +[plugins] +pkl = { id = "org.pkl-lang", version.ref = "pkl" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1af9e09 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/basePklProject.pkl b/packages/basePklProject.pkl new file mode 100644 index 0000000..f3eaca3 --- /dev/null +++ b/packages/basePklProject.pkl @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// +module basePklProject + +amends "pkl:Project" + +import "pkl:reflect" + +local myModule = reflect.Module(module) + +local packageName: String = + findRootModule(reflect.Module(module)) + .relativePathTo(module) + .last + +local function findRootModule(mod: reflect.Module): Module = + let (supermodule = mod.supermodule) + if (supermodule == null || !supermodule.isAmend) mod.reflectee + else findRootModule(supermodule) + +local allTests = import*("**/tests/**.pkl").keys.filter((it) -> !it.contains("tests/fixtures/")) + +package { + name = packageName + apiTests = tests // api tests are shared with module tests + baseUri = "package://pkg.pkl-lang.org/pkl-project-commons/\(name)" + packageZipUrl = "https://github.com/apple/pkl-project-commons/releases/download/\(name)@\(version)/\(name)@\(version).zip" + license = "Apache-2.0" + authors { + "The Pkl Authors " + } + exclude { + "examples/**" + "tests/**" + } + description = myModule.docComment + issueTracker = "https://github.com/apple/pkl-project-commons/issues" + sourceCode = "https://github.com/apple/pkl-project-commons/tree/\(name)@\(version)/packages/\(name)" + sourceCodeUrlScheme = "https://github.com/apple/pkl-project-commons/blob/\(name)@\(version)/packages/\(name)%{path}#L%{line}-%{endLine}" +} + +tests { + for (test in allTests) { + when (test.split("/").first == package.name) { + test.replaceFirst(package.name, ".") + } + } +} diff --git a/packages/pkl.impl.circleci/CircleCI.pkl b/packages/pkl.impl.circleci/CircleCI.pkl new file mode 100644 index 0000000..8cfd0be --- /dev/null +++ b/packages/pkl.impl.circleci/CircleCI.pkl @@ -0,0 +1,726 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// +/// Schema for CircleCI's `config.yml`. +/// +/// This module is a work in progress, if you find some unsuported feature +/// feel free to add it. +module com.circleci.v2.CircleCI + +/// Version of the `config.yml` schema to target. +/// +/// Currently, only version 2.1 is supported by this template. +version: "2.1" + +/// Designates the config.yaml for use of CircleCI’s +/// [dynamic configuration](https://circleci.com/docs/dynamic-config/) feature. +setup: Boolean? + +/// A map of user-selected names to either: orb references (strings) or orb definitions (maps). +/// +/// Orb definitions must be the orb-relevant subset of 2.1 config. +/// See the [Creating Orbs](https://circleci.com/docs/creating-orbs/) documentation for details. +orbs: Mapping? + +/// A command defines a sequence of steps as a map to be executed in a job, enabling you to reuse +/// a single command definition across multiple jobs. +/// +/// For more information see the +/// [Reusable Config Reference Guide](https://circleci.com/docs/reusing-config/). +commands: Mapping? + +/// A Workflow is comprised of one or more uniquely named jobs. +/// +/// Jobs are specified in the jobs map, see +/// [Sample config.yml](https://circleci.com/docs/sample-config/) for two examples of a job map. +/// The name of the job is the key in the map, and the value is a map describing the job. +jobs: Mapping? + +/// Used for orchestrating all jobs. +/// +/// Each workflow consists of the workflow name as a key and a map as a value. +/// A name should be unique within the current config.yml. +/// The top-level keys for the Workflows configuration are version and jobs. +/// For more information, see the +/// [Using Workflows to Orchestrate Jobs](https://circleci.com/docs/workflows/) page. +workflows: Mapping? + +/// Pipeline parameters declared for use in the configuration. +/// +/// See [Pipeline Values and Parameters](https://circleci.com/docs/pipeline-variables/#pipeline-parameters-in-configuration) +/// for usage details. +parameters: Mapping? + +class Orb { + /// A map of user-selected names to either: orb references (strings) or orb definitions (maps). + /// + /// Orb definitions must be the orb-relevant subset of 2.1 config. + /// See the [Creating Orbs](https://circleci.com/docs/creating-orbs/) documentation for details. + orbs: Mapping? + + /// A command defines a sequence of steps as a map to be executed in a job, enabling you to reuse + /// a single command definition across multiple jobs. + /// + /// For more information see the + /// [Reusable Config Reference Guide](https://circleci.com/docs/reusing-config/). + commands: Mapping? + + /// A Workflow is comprised of one or more uniquely named jobs. + /// + /// Jobs are specified in the jobs map, see + /// [Sample config.yml](https://circleci.com/docs/sample-config/) for two examples of a job map. + /// The name of the job is the key in the map, and the value is a map describing the job. + jobs: Mapping? +} + +class Job { + /// Shell to use for execution command in all steps. + /// + /// Can be overridden by shell in each step (default: See Default Shell Options) + shell: String? + + /// A list of [steps](https://circleci.com/docs/configuration-reference/#steps) to be performed + steps: Listing(!isEmpty) + + /// In which directory to run the steps. + /// + /// Will be interpreted as an absolute path. + /// + /// Default: `~/project` (where project is a literal string, not the name of your specific project). + /// + /// Processes run during the job can use the `$CIRCLE_WORKING_DIRECTORY` environment variable to + /// refer to this directory. + /// + /// Note: Paths written in your YAML configuration file will not be expanded; if your + /// store_test_results.path is $CIRCLE_WORKING_DIRECTORY/tests, then CircleCI will attempt to + /// store the test subdirectory of the directory literally named $CIRCLE_WORKING_DIRECTORY, + /// dollar sign $ and all. working_directory will be created automatically if it doesn’t exist. + working_directory: String? + + /// Number of parallel instances of this job to run (default: 1) + parallelism: Int? + + /// A map of environment variable names and values. + environment: Mapping? + + /// The [resource_class](https://circleci.com/docs/resource-class-overview/) feature allows you to configure CPU and RAM resources for each job. + /// + /// Resource classes are available for each execution environment, as described in the tables below. + /// + /// We implement soft concurrency limits for each resource class to ensure our system remains + /// stable for all customers. + /// If you are on a Performance or custom plan and experience queuing for certain resource + /// classes, it is possible you are hitting these limits. + /// Contact [CircleCI support](https://support.circleci.com/hc/en-us/requests/new) to request a + /// raise on these limits for your account. + /// + /// If you do not specify a resource class, CircleCI will use a default value that is subject + /// to change. + /// It is best practice to specify a resource class as opposed to relying on a default. + resource_class: ResourceClass? + + /// Options for [docker executor](https://circleci.com/docs/configuration-reference/#docker) + docker: Listing(!isEmpty)?(onlyOneSet(List(this, macos, machine))) + + /// CircleCI supports running jobs on macOS, to allow you to build, test, and deploy apps for + /// macOS, iOS, tvOS and watchOS. + /// + /// To run a job in a macOS virtual machine, add the macos key to the top-level configuration + /// for your job and specify the version of Xcode you would like to use. + macos: MacOSExecutor? + + /// Options for [machine executor](https://circleci.com/docs/configuration-reference/#machine) + machine: Machine? +} + +typealias ResourceClass = + "small" + |"medium" + |"medium+" + |"large" + |"xlarge" + |"2xlarge" + |"2xlarge+" + |"arm.medium" + |"arm.large" + |"arm.xlarge" + |"arm.2xlarge" + |"macos.m1.medium.gen1" + |"macos.m1.large.gen1" + +class MacOSExecutor { + /// The version of Xcode that is installed on the virtual machine, see the + /// [Supported Xcode Versions section of the Testing iOS document](https://circleci.com/docs/using-macos/#supported-xcode-versions) + /// for the complete list. + xcode: String +} + +class Machine { + /// The virtual machine image to use. + /// + /// View [available images](https://circleci.com/developer/images?imageType=machine). + /// + /// Note: This key is *not* supported for Linux VMs on installations of CircleCI server. + /// For information about customizing `machine` executor images on CircleCI installed on your + /// servers, see our [VM Service documentation](https://circleci.com/docs/server/v4.1/operator/manage-virtual-machines-with-vm-service/) + image: String + + /// Set this to `true` to enable [Docker Layer Caching](https://circleci.com/docs/docker-layer-caching/). + docker_layer_caching: Boolean? +} + +class DockerImage { + /// The name of a custom docker image to use. + /// + /// The first `image` listed under a job defines the job’s own primary container image where all + /// steps will run. + image: String + + /// `name` defines the the hostname for the container (the default is `localhost`), which is used + /// for reaching secondary (service) containers. + /// + /// By default, all services are exposed directly on `localhost`. + /// This field is useful if you would rather have a different hostname instead of `localhost`, + /// for example, if you are starting multiple versions of the same service. + name: String? + + /// The command used as executable when launching the container. + /// + /// [entrypoint] overrides the image’s `ENTRYPOINT` + entrypoint: (*Listing|String)? + + /// The command used as pid 1 (or args for entrypoint) when launching the container. + /// + /// [command] overrides the image’s `COMMAND`. + /// It will be used as arguments to the image `ENTRYPOINT` if it has one, or as the executable + /// if the image has no `ENTRYPOINT`. + command: (*Listing|String)? + + /// Which user to run commands as within the Docker container + user: String? + + /// environment variable names and values. + /// + /// The [environment] settings apply to the entrypoint/command run by the docker container, not + /// the job steps + environment: Mapping? + + /// Authentication for registries using standard docker login credentials + auth: Mapping? + + /// Authentication for AWS Elastic Container Registry (ECR) + aws_auth: Mapping? +} + +class ScheduleTrigger { + /// The cron key is defined using POSIX crontab syntax + @SourceCode { language = "cronexp" } + cron: String + + /// Trigger Filters can have the key branches. + filters: ScheduleTriggerFilter +} + +class ScheduleTriggerFilter { + /// The `branches` key controls whether the _current_ branch should have a schedule trigger + /// created for it, where _current_ branch is the branch containing the `config.yml` file with + /// the trigger stanza. + /// That is, a push on the `main` branch will only schedule a + /// [workflow](https://circleci.com/docs/workflows/#using-contexts-and-filtering-in-your-workflows) + /// for the `main` branch. + /// + /// Branches can have the keys only and ignore which each map to a single string naming a branch. + /// You may also use regular expressions to match against branches by enclosing them with `/`’s, + /// or map to a list of such strings. Regular expressions must match the **entire** string. + /// + /// * Any branches that match only will run the job. + /// * Any branches that match ignore will not run the job. + /// * If neither only nor ignore are specified then all branches will run the job. + /// If both only and ignore are specified, the only is used and ignore will have no effect. + branches: ScheduleTriggerFilterBranches +} + +class ScheduleTriggerFilterBranches { + /// Either a single branch specifier, or a list of branch specifiers + only: *Listing|String + + /// Either a single branch specifier, or a list of branch specifiers + ignore: (*Listing|String)? +} + +class Workflow { + /// A job can have the keys `requires`, `name`, `context`, `type`, and `filters`. + jobs: Listing<*Mapping(length == 1)|String>(!isEmpty) + + /// Specifies which triggers will cause this workflow to be executed. + /// + /// Default behavior is to trigger the workflow when pushing to a branch + triggers: Listing? + + `when`: (*LogicStatement|Boolean|String)? + + `unless`: (*LogicStatement|Boolean|String)? +} + +class WorkflowJob { + /// The name key can be used to invoke reusable jobs across any number of workflows. + /// + /// Using the name key ensures numbers are not appended to your job name (i.e. sayhello-1, + /// sayhello-2, etc.). + /// The name you assign to the name key needs to be unique, otherwise the numbers will still be + /// appended to the job name. + name: String? + + /// A list of jobs that must succeed for the job to start. + /// + /// Note: When jobs in the current workflow that are listed as dependencies are not executed + /// (due to a filter function for example), their requirement as a dependency for other jobs will + /// be ignored by the requires option. + /// However, if all dependencies of a job are filtered, then that job will not be executed either. + requires: Listing? + + /// The name of the context(s). + /// + /// Jobs may be configured to use global environment variables set for an organization, see the + /// [Contexts](https://circleci.com/docs/contexts/) document for adding a context in the + /// application settings. + /// + /// The initial default name is org-global. + /// Each context name must be unique. + /// If using CircleCI Server, only a single Context per workflow is supported. + /// Note: A maximum of 100 unique contexts across all workflows is allowed + context: (*Listing|String)? + + /// A job may have a type of `approval` indicating it must be manually approved before downstream + /// jobs may proceed. + /// + /// For more information see the Using Workflows to Orchestrate Jobs page. + type: "approval"? + + /// Job Filters can have the key branches or tags + filters: JobFilters? +} + +class JobFilters { + /// Branches can have the keys only and ignore which either map to a single string naming a branch. + /// You may also use regular expressions to match against branches by enclosing them with slashes, + /// or map to a list of such strings. + /// Regular expressions must match the *entire* string. + /// + /// Any branches that match `only` will run the job. + /// Any branches that match `ignore` will not run the job. + /// If neither `only` nor `ignore` are specified then all branches will run the job. + /// If both `only` and `ignore` are specified the `only` is considered before `ignore`. + branches: FilterSpec? + + /// CircleCI does not run workflows for tags unless you explicitly specify tag filters. + /// Additionally, if a job requires any other jobs (directly or indirectly), you must specify tag + /// filters for those jobs. + /// + /// Tags can have the keys only and ignore. + /// You may also use regular expressions to match against tags by enclosing them with slashes, + /// or map to a list of such strings. + /// Regular expressions must match the entire string. + /// Both lightweight and annotated tags are supported. + /// + /// Any tags that match only will run the job. + /// Any tags that match ignore will not run the job. + /// If neither only nor ignore are specified then the job is skipped for all tags. + /// If both only and ignore are specified the only is considered before ignore. + tags: FilterSpec? +} + +class FilterSpec { + /// Either a single branch specifier, or a list of branch specifiers + only: (String|Listing)?(this != null || ignore != null) + + /// Either a single branch specifier, or a list of branch specifiers + ignore: (String|Listing)? +} + +typealias Step = AbstractStep|SimpleStepName + +typealias SimpleStepName = "checkout"|"setup_remote_docker"|"add_ssh_keyes"|String + +local abstract class AbstractStep { + fixed hidden __name__: String +} + +function run(_command: String): RunStep = new { + command = _command +} + +/// Used for invoking all command-line programs. +/// +/// Run commands are executed using non-login shells by default, so you must explicitly source any +/// dotfiles as part of the command. +class RunStep extends AbstractStep { + fixed hidden __name__ = "run" + + /// Command to run via the shell + command: String(!isEmpty) + + /// Title of the step to be shown in the CircleCI UI (default: full [command]) + name: String? + + /// Shell to use for execution command (default: See [Default Shell Options](https://circleci.com/docs/configuration-reference/#default-shell-options)) + shell: String? + + /// Additional environmental variables, locally scoped to command + environment: Mapping? + + /// Whether or not this step should run in the background (default: [false]) + background: Boolean? + + /// In which directory to run this step. + /// + /// Will be interpreted relative to the working_directory + /// of the job. (default: `.`) + working_directory: String? + + /// Elapsed time the command can run without output. + /// + /// The default is 10 minutes and the maximum is governed by the maximum time a job is allowed to run. + no_output_timeout: Duration? + + /// Specify when to enable or disable the step. + /// + /// Takes the following values: `always`, `on_success`, `on_fail` (default: on_success) + `when`: ("always"|"on_success"|"on_fail")? +} + +/// Generates and stores a cache of a file or directory of files such as dependencies or source +/// code in our object storage. +/// +/// Later jobs can [restore this cache](https://circleci.com/docs/configuration-reference/#restore_cache). +/// Learn more on the [Caching Dependencies](https://circleci.com/docs/caching/) page. +/// +/// Cache retention can be customized on the [CircleCI web app](https://app.circleci.com/) by +/// navigating to Plan > Usage Controls. +class SaveCacheStep extends AbstractStep { + fixed hidden __name__ = "save_cache" + + /// List of directories which should be added to the cache + paths: Listing + + /// Unique identifier for this cache + key: String + + /// Title of the step to be shown in the CircleCI UI (default: “Saving Cache”) + name: String? + + /// [Specify when to enable or disable the step](https://circleci.com/docs/configuration-reference/#the-when-attribute). + /// + /// Takes the following values: always, on_success, on_fail (default: on_success) + `when`: ("always"|"on_success"|"on_fail")? +} + +/// Restores a previously saved cache based on a key. +/// +/// Cache needs to have been saved first for this key using the [save_cache] step. +/// +/// Learn more in [the caching documentation](https://circleci.com/docs/caching/). +class RestoreCacheStep extends AbstractStep { + fixed hidden __name__ = "restore_cache" + + /// Single cache key to restore + key: String?(this != null || keys != null) + + /// List of cache keys to lookup for a cache to restore. + /// + /// Only first existing key will be restored. + keys: Listing? + + /// Title of the step to be shown in the CircleCI UI (default: “Restoring Cache”) + name: String? +} + +/// Allows Docker commands to be run locally. +/// +/// See [Running Docker Commands](https://circleci.com/docs/building-docker-images/) for details. +class SetupRemoteDockerStep extends AbstractStep { + fixed hidden __name__ = "setup_remote_docker" + + /// Version string of Docker you would like to use (default: 20.10.17). + /// + /// View the list of supported docker versions + /// [here](https://circleci.com/docs/building-docker-images/#docker-version). + version: String + + /// Set this to true to enable [Docker Layer Caching](https://circleci.com/docs/docker-layer-caching/) + /// in the Remote Docker Environment (default: false) + docker_layer_cacheing: Boolean +} + +/// Special step used to persist a temporary file to be used by another job in the workflow. +/// For more information on using workspaces, see the +/// [Using Workspaces to Share Data Between Jobs](https://circleci.com/docs/workspaces/) page. +/// +/// `persist_to_workspace` adopts the storage settings from the storage customization controls on +/// the CircleCI web app. +/// If no custom setting is provided, `persist_to_workspace` defaults to 15 days. +/// +/// Workspace storage retention can be customized on the CircleCI web app by navigating to Plan > Usage Controls. +class PersistToWorkspaceStep extends AbstractStep { + fixed hidden __name__ = "persist_to_workspace" + + /// Either an absolute path or a path relative to `working_directory` + root: String + + /// Glob identifying file(s), or a non-glob path to a directory to add to the shared workspace. + /// + /// Interpreted as relative to the workspace root. + /// Must not be the workspace root itself + paths: Listing +} + +/// Special step used to attach the workflow’s workspace to the current container. +/// +/// The full contents of the workspace are downloaded and copied into the directory the workspace +/// is being attached at. +/// For more information on using workspaces, see the +/// [Using Workspaces to Share Data Between Jobs](https://circleci.com/docs/workspaces/) page. +class AttachWorkspaceStep extends AbstractStep { + fixed hidden __name__ = "attach_workspace" + + /// Directory to attach the workspace to. + at: String +} + +/// A conditional step consists of a step with the key `when`. +/// +/// Under the `when` key are the subkeys `condition` and `steps`. +/// The purpose of the `when` step is customizing commands and job configuration to run on custom +/// conditions (determined at config-compile time) that are checked before a workflow runs. +/// See the [Conditional Steps section of the Reusing Config](https://circleci.com/docs/reusing-config/#defining-conditional-steps) +/// document for more details. +class WhenStep { + fixed hidden __name__ = "when" + + /// The logic statement that determines whether to execute. + condition: (*LogicStatement|Boolean|String)? + + /// A list of steps to execute when the condition is true + steps: Listing? +} + +/// A conditional step consists of a step with the key `unless`. +/// +/// Under the `unless` key are the subkeys `condition` and `steps`. +/// The purpose of the `unless` step is customizing commands and job configuration to run on custom +/// conditions (determined at config-compile time) that are checked before a workflow runs. +/// See the [Conditional Steps section of the Reusing Config](https://circleci.com/docs/reusing-config/#defining-conditional-steps) +/// document for more details. +class UnlessStep { + fixed hidden __name__ = "unless" + + /// The logic statement that determines whether to execute. + condition: (*LogicStatement|Boolean|String)? + + /// A list of steps to execute when the condition is true + steps: Listing? +} + +class Command { + /// A sequence of steps run inside the calling job of the command. + steps: Listing(!isEmpty) + + /// A map of parameter keys. + /// + /// See the [Parameter Syntax](https://circleci.com/docs/reusing-config/#parameter-syntax) + /// section of the [Reusing Config](https://circleci.com/docs/reusing-config/) document for details. + parameters: Mapping? + + /// A string that describes the purpose of the command. + description: String? +} + +class Parameter { + /// Optional. Used to generate documentation for your orb. + description: String? + + /// The default value for the parameter. If not present, the parameter is implied to be required. + default: (String|Number|Boolean)? + + /// Required. See [Parameter Types](https://circleci.com/docs/reusing-config/#parameter-types) + /// for details. + type: "string"|"boolean"|"integer"|"enum"|"executor"|"steps"|"env_var_name" +} + +/// Special step used to upload and store test results for a build. +/// +/// Test results are visible on the CircleCI web application under each build’s *Test Summary* +/// section. +/// Storing test results is useful for timing analysis of your test suites. +/// For more information on storing test results, see the +/// [Collecting Test Data](https://circleci.com/docs/collect-test-data/) page. +/// +/// It is also possible to store test results as a build artifact; to do so, please refer to the +/// [store_artifacts] step. +class StoreTestResults extends AbstractStep { + fixed hidden __name__ = "store_test_results" + + /// Path (absolute, or relative to your `working_directory`) to directory containing JUnit XML + /// test metadata files, or to a single test file. + path: String +} + +/// Step to store artifacts (for example logs, binaries, etc) to be available in the web app +/// or through the API. +/// +/// See the [Uploading Artifacts](https://circleci.com/docs/artifacts/) document for more information. +class StoreArtifacts extends AbstractStep { + fixed hidden __name__ = "store_artifacts" + + /// Directory in the primary container to save as job artifacts + path: String + + /// Prefix added to the artifact paths in the artifacts API (default: the directory of the file + /// specified in path) + destination: String? +} + +/// Certain dynamic configuration features accept logic statements as arguments. +/// +/// Logic statements are evaluated to boolean values at configuration compilation time, that is, +/// before the workflow is run. +class LogicStatement { + /// True if all arguments are truthy. + and: Listing<*LogicStatement|Boolean|String>?(onlyOneSet(List(this, or, not, equal, matches))) + + /// True if any arguments are truthy. + or: Listing<*LogicStatement|Boolean|String>? + + /// True if the argument is not truthy. + not: (*LogicStatement|Boolean|String)? + + /// True if all arguments evaluate to equal values. + equal: Listing? + + /// True if [value][Match.value] matches the [pattern][Match.pattern]. + matches: Match? +} + +/// Tells if only one of the values is not null. +local const function onlyOneSet(values: List): Boolean = + values.filter((it) -> it != null).length == 1 + +class Match { + /// The value to match against + value: String + + /// A [Java regular expression](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html) + /// used to test against the value. + /// + /// A full match pattern must be provided, prefix matching is not an option. + /// Though, it is recommended to enclose a pattern in `^` and `$` to avoid accidental partial matches. + @SourceCode { language = "RegExp" } + pattern: String +} + +typealias CalledCommand = Mapping(length == 1) + +typealias CommandCall = Mapping + +class PipelineValues { + /// A globally unique id representing for the pipeline. + /// + /// Type: string + `pipeline.id`: "<< pipeline.id >>" + + /// A project unique integer id for the pipeline. + /// + /// Type: integer + `pipeline.number`: "<< pipeline.number >>" + + /// The URL where the current project is hosted. + /// + /// For example, `https://github.com/circleci/circleci-docs`. + `pipeline.project.git_url`: "<< pipeline.project.git_url >>" + `pipeline.project.type`: "<< pipeline.project.type >>" + `pipeline.git.tag`: "<< pipeline.git.tag >>" + `pipeline.git.branch`: "<< pipeline.git.branch >>" + `pipeline.git.revision`: "<< pipeline.git.revision >>" + `pipeline.git.base_revision`: "<< pipeline.git.base_revision >>" + `pipeline.in_setup`: "<< pipeline.in_setup >>" + `pipeline.trigger_source`: "<< pipeline.trigger_source >>" + `pipeline.schedule.name`: "<< pipeline.schedule.name >>" + `pipeline.schedule.id`: "<< pipeline.schedule.id >>" + `pipeline.trigger_parameters.circleci.trigger_type`: "<< pipeline.trigger_parameters.circleci.trigger_type >>" + `pipeline.trigger_parameters.circleci.event_time`: "<< pipeline.trigger_parameters.circleci.event_time >>" + `pipeline.trigger_parameters.circleci.event_type`: "<< pipeline.trigger_parameters.circleci.event_type >>" + `pipeline.trigger_parameters.circleci.project_id`: "<< pipeline.trigger_parameters.circleci.project_id >>" + `pipeline.trigger_parameters.circleci.actor_id`: "<< pipeline.trigger_parameters.circleci.actor_id >>" + `pipeline.trigger_parameters.gitlab.type`: "<< pipeline.trigger_parameters.gitlab.type >>" + `pipeline.trigger_parameters.github_app.type`: "<< pipeline.trigger_parameters.github_app.type >>" + `pipeline.trigger_parameters.gitlab.project_id`: "<< pipeline.trigger_parameters.gitlab.project_id >>" + `pipeline.trigger_parameters.github_app.project_id`: "<< pipeline.trigger_parameters.github_app.project_id >>" + `pipeline.trigger_parameters.gitlab.ref`: "<< pipeline.trigger_parameters.gitlab.ref >>" + `pipeline.trigger_parameters.github_app.ref`: "<< pipeline.trigger_parameters.github_app.ref >>" + `pipeline.trigger_parameters.gitlab.checkout_sha`: "<< pipeline.trigger_parameters.gitlab.checkout_sha >>" + `pipeline.trigger_parameters.github_app.checkout_sha`: "<< pipeline.trigger_parameters.github_app.checkout_sha >>" + `pipeline.trigger_parameters.gitlab.user_id`: "<< pipeline.trigger_parameters.gitlab.user_id >>" + `pipeline.trigger_parameters.github_app.user_id`: "<< pipeline.trigger_parameters.github_app.user_id >>" + `pipeline.trigger_parameters.gitlab.user_name`: "<< pipeline.trigger_parameters.gitlab.user_name >>" + `pipeline.trigger_parameters.github_app.user_name`: "<< pipeline.trigger_parameters.github_app.user_name >>" + `pipeline.trigger_parameters.gitlab.user_username`: "<< pipeline.trigger_parameters.gitlab.user_username >>" + `pipeline.trigger_parameters.github_app.user_username`: "<< pipeline.trigger_parameters.github_app.user_username >>" + `pipeline.trigger_parameters.gitlab.user_avatar`: "<< pipeline.trigger_parameters.gitlab.user_avatar >>" + `pipeline.trigger_parameters.github_app.user_avatar`: "<< pipeline.trigger_parameters.github_app.user_avatar >>" + `pipeline.trigger_parameters.gitlab.repo_name`: "<< pipeline.trigger_parameters.gitlab.repo_name >>" + `pipeline.trigger_parameters.github_app.repo_name`: "<< pipeline.trigger_parameters.github_app.repo_name >>" + `pipeline.trigger_parameters.gitlab.repo_url`: "<< pipeline.trigger_parameters.gitlab.repo_url >>" + `pipeline.trigger_parameters.github_app.repo_url`: "<< pipeline.trigger_parameters.github_app.repo_url >>" + `pipeline.trigger_parameters.gitlab.web_url`: "<< pipeline.trigger_parameters.gitlab.web_url >>" + `pipeline.trigger_parameters.github_app.web_url`: "<< pipeline.trigger_parameters.github_app.web_url >>" + `pipeline.trigger_parameters.gitlab.commit_sha`: "<< pipeline.trigger_parameters.gitlab.commit_sha >>" + `pipeline.trigger_parameters.github_app.commit_sha`: "<< pipeline.trigger_parameters.github_app.commit_sha >>" + `pipeline.trigger_parameters.gitlab.commit_title`: "<< pipeline.trigger_parameters.gitlab.commit_title >>" + `pipeline.trigger_parameters.github_app.commit_title`: "<< pipeline.trigger_parameters.github_app.commit_title >>" + `pipeline.trigger_parameters.gitlab.commit_message`: "<< pipeline.trigger_parameters.gitlab.commit_message >>" + `pipeline.trigger_parameters.github_app.commit_message`: "<< pipeline.trigger_parameters.github_app.commit_message >>" + `pipeline.trigger_parameters.gitlab.commit_timestamp`: "<< pipeline.trigger_parameters.gitlab.commit_timestamp >>" + `pipeline.trigger_parameters.github_app.commit_timestamp`: "<< pipeline.trigger_parameters.github_app.commit_timestamp >>" + `pipeline.trigger_parameters.gitlab.commit_author_name`: "<< pipeline.trigger_parameters.gitlab.commit_author_name >>" + `pipeline.trigger_parameters.github_app.commit_author_name`: "<< pipeline.trigger_parameters.github_app.commit_author_name >>" + `pipeline.trigger_parameters.gitlab.commit_author_email`: "<< pipeline.trigger_parameters.gitlab.commit_author_email >>" + `pipeline.trigger_parameters.github_app.commit_author_email`: "<< pipeline.trigger_parameters.github_app.commit_author_email >>" + `pipeline.trigger_parameters.gitlab.total_commits_count`: "<< pipeline.trigger_parameters.gitlab.total_commits_count >>" + `pipeline.trigger_parameters.github_app.total_commits_count`: "<< pipeline.trigger_parameters.github_app.total_commits_count >>" + `pipeline.trigger_parameters.gitlab.branch`: "<< pipeline.trigger_parameters.gitlab.branch >>" + `pipeline.trigger_parameters.github_app.branch`: "<< pipeline.trigger_parameters.github_app.branch >>" + `pipeline.trigger_parameters.gitlab.default_branch`: "<< pipeline.trigger_parameters.gitlab.default_branch >>" + `pipeline.trigger_parameters.github_app.default_branch`: "<< pipeline.trigger_parameters.github_app.default_branch >>" + `pipeline.trigger_parameters.gitlab.x_gitlab_event_id`: "<< pipeline.trigger_parameters.gitlab.x_gitlab_event_id >>" + `pipeline.trigger_parameters.gitlab.is_fork_merge_request`: "<< pipeline.trigger_parameters.gitlab.is_fork_merge_request >>" +} + +/// Pipeline values are available to all pipeline configurations and can be used without previous +/// declaration. +/// +/// For a full list of values and built-in environment variables, see the +/// [Project values and variables guide](https://circleci.com/docs/variables/). +/// +/// For more reference: +hidden pipelineValues: PipelineValues + +output { + text = "# Generated from CircleCI.pkl. DO NOT EDIT.\n" + super.text + renderer = new YamlRenderer { + converters { + [AbstractStep] = (it) -> Map(it.__name__, it.toMap()) + } + } +} diff --git a/packages/pkl.impl.circleci/PklCI.pkl b/packages/pkl.impl.circleci/PklCI.pkl new file mode 100644 index 0000000..516564c --- /dev/null +++ b/packages/pkl.impl.circleci/PklCI.pkl @@ -0,0 +1,245 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// +/// CI job template specifically for Pkl projects. +module pkl.impl.circleci.PklCI + +// TODO: Undo this hack to break the cycle; pkl-pantry depends on pkl-project-commons and vice-versa. +// Copied in `CircleCI.pkl` locally, so that `pkl-project-commons` builds green and publishes. +// Releasing `pkl-project-commons` first allows `pkl-pantry` to be released dependent on it. After `pkl-pantry` +// is public, delete `CircleCI.pkl` from this repository and use the `package://` import below. +//import "@circleci/CircleCI.pkl" +import "CircleCI.pkl" + +/// A map of user-selected names to either: orb references (strings) or orb definitions (maps). +/// +/// Orb definitions must be the orb-relevant subset of 2.1 config. +/// See the [Creating Orbs](https://circleci.com/docs/creating-orbs/) documentation for details. +orbs: Mapping? + +/// The jobs required by this build +jobs: Mapping + +/// The workflow to run for pull request builds. +prb: CircleCI.Workflow + +/// The workflow to run for commits on the main branch +main: CircleCI.Workflow + +/// The workflow to run for tags that match a semver. +/// +/// If [null], this workflow is omitted. +release: CircleCI.Workflow? + +/// Whether to trigger a build of the Pkl docsite or not. +/// +/// * `"none"` - does not trigger build after the [release] workflow (default) +/// * `"release"` - trigger build after the [release] workflow only. +/// * `"both"` - trigger build for the [release] workflow and the [main] workflow. +/// `both` should only be used for projects where we publish documentation +/// for dev/snapshot builds. +triggerDocsBuild: "none"|"release"|"both" = "none" + +/// Whether to trigger a build of pkl-package-docs or not. +/// +/// * `"none"` -- does not trigger a build after the [release] workflow (default) +/// * `"release"` -- trigger a build after the [release] workflow +/// * `"main"` -- trigger a build after the [main] workflow +triggerPackageDocsBuild: "none"|"main"|"release" = "none" + +local function leafJobNames(jobs: Listing|String>) = + let ( + requiredJobs = jobs.toList() + .filterIsInstance(Mapping) + .flatMap((it: Mapping) -> it[it.keys.first].requires?.toList() ?? List()) + .toSet() + ) + let (allJobNames = jobs.toList().map((it) -> if (it is String) it else it.keys.first).toSet()) + allJobNames.difference(requiredJobs) + +local function withTriggerDownstreamJob(jobName: String): Mixin|String>> = (it) -> (it) { + new { + [jobName] { + requires = leafJobNames(it).toListing() + context { + "pkl-pr-approval" + } + } + } +} + +local identity = (it) -> it + +/// The CircleCI output. +circleCi: CircleCI = new { + version = "2.1" + orbs = (module.orbs) { + ["pr-approval"] = "apple/pr-approval@0.1.0" + } + jobs = (module.jobs) { + when (triggerDocsBuild != "none") { + ["trigger-docsite-build"] { + docker { + new { image = "cimg/base:current" } + } + steps { + new RunStep { + name = "Triggering docsite build" + command = #""" + curl --location \ + --request POST \ + --header "Content-Type: application/json" \ + -u "${CIRCLE_TOKEN}:" \ + --data '{ "branch": "main" }' \ + "https://circleci.com/api/v2/project/github/apple/pkl-lang.org/pipeline" + """# + } + } + } + } + when (triggerPackageDocsBuild != "none") { + ["trigger-package-docs-build"] { + docker { + new { image = "cimg/base:current" } + } + steps { + new RunStep { + name = "Triggering docsite build" + command = #""" + curl --location \ + --request POST \ + --header "Content-Type: application/json" \ + -u "${CIRCLE_TOKEN}:" \ + --data '{ "branch": "main" }' \ + "https://circleci.com/api/v2/project/github/apple/pkl-package-docs/pipeline" + """# + } + } + } + } + } + workflows { + ["prb"] = (prb) { + `when` { + matches { + pattern = "^pull/\\d+(/head)?$" + value = "<< pipeline.git.branch >>" + } + } + jobs = new { + new { + ["hold"] { + type = "approval" + } + } + new { + ["pr-approval/authenticate"] { + context = "pkl-pr-approval" + } + } + for (job in prb.jobs) { + job |> requireApproval + } + } + } + ["main"] = (main) { + `when` { + equal { + "main" + CircleCI.pipelineValues.`pipeline.git.branch` + } + } + jobs = super.jobs + |> (if (triggerPackageDocsBuild == "main") withTriggerDownstreamJob("trigger-package-docs-build") else identity) + |> (if (triggerDocsBuild == "both") withTriggerDownstreamJob("trigger-docsite-build") else identity) + } + when (release != null) { + ["release"] = (release) { + // Can't use `when` for building on tags. + // See: https://discuss.circleci.com/t/cant-trigger-workflow-on-git-tag-push-using-when-condition/43252 + jobs = super.jobs + |> (if (triggerPackageDocsBuild == "release") withTriggerDownstreamJob("trigger-package-docs-build") else identity) + |> (if (triggerDocsBuild == "release") withTriggerDownstreamJob("trigger-docsite-build") else identity) + |> runOnTag + } + } + } +} + +local runOnTag = (jobs: Listing|String>) -> + new Listing<*Mapping|String> { + for (job in jobs) { + when (job is String) { + new { + [job] { + filters { + tags { + only = #"/^v?\d+\.\d+\.\d+$/"# + } + branches { + ignore = "/.*/" + } + } + } + } + } else { + (job) { + [job.keys.first] { + filters { + tags { + only = #"/^v?\d+\.\d+\.\d+$/"# + } + branches { + ignore = "/.*/" + } + } + } + } + } + } + } + +local requireApproval = (it: String|Mapping) -> + if (it is String) + new Mapping { + [it] { + requires { "hold"; "pr-approval/authenticate" } + } + } + else + (it) { + [it.keys.first] { + requires { "hold"; "pr-approval/authenticate" } + } + } + +output = circleCi.output + +// re-export types so they are available without qualification +typealias StoreTestResults = CircleCI.StoreTestResults +typealias SaveCacheStep = CircleCI.SaveCacheStep +typealias AttachWorkspaceStep = CircleCI.AttachWorkspaceStep +typealias RunStep = CircleCI.RunStep +typealias PersistToWorkspaceStep = CircleCI.PersistToWorkspaceStep +typealias RestoreCacheStep = CircleCI.RestoreCacheStep +typealias SetupRemoteDockerStep = CircleCI.SetupRemoteDockerStep +typealias UnlessStep = CircleCI.UnlessStep +typealias WhenStep = CircleCI.WhenStep +typealias LogicStatement = CircleCI.LogicStatement +typealias Job = CircleCI.Job +typealias JobFilters = CircleCI.JobFilters +typealias Workflow = CircleCI.Workflow +typealias WorkflowJob = CircleCI.WorkflowJob +typealias Orb = CircleCI.Orb diff --git a/packages/pkl.impl.circleci/PklProject b/packages/pkl.impl.circleci/PklProject new file mode 100644 index 0000000..1a9b15b --- /dev/null +++ b/packages/pkl.impl.circleci/PklProject @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// +amends "../basePklProject.pkl" + +package { + version = "1.0.0" +} + +dependencies { +// TODO(After release of this and pkl-pantry): Cut over to `pkl-pantry` dependency +// ["circleci"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/com.circleci@1.0.0" } +} diff --git a/packages/pkl.impl.circleci/PklProject.deps.json b/packages/pkl.impl.circleci/PklProject.deps.json new file mode 100644 index 0000000..836079a --- /dev/null +++ b/packages/pkl.impl.circleci/PklProject.deps.json @@ -0,0 +1,4 @@ +{ + "schemaVersion": 1, + "resolvedDependencies": {} +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..188b31f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "pkl-project-commons"