- a feature request: [typesafegithub/github-workflows-kt] [Core feature request] Pinning action versions to commit hashes updateable by bots
When consuming an action through the bindings server, using the most popular pattern, like so:
@file:Repository("https://bindings.krzeminski.it")
@file:DependsOn("actions:checkout:v4")the script may get a different artifact each time the script is compiled, and a different action may be run when the workflow is run.
It's because unlike with Maven Central, the Maven-compatible custom bindings server doesn't have an inherent property of
artifact immutability (on why it's not possible, see
Pinning to a specific binding (e.g. by JAR's checksum)). The v4
version is usually modeled as a moving tag/branch, pointing to the newest released version from major version 4.
Even tags with full version:
@file:DependsOn("actions:checkout:v4.1.2")aren't guaranteed to be immutable. They should be by convention, but there's no mechanism to enforce it.
It's sometimes desired to pin to a specific action logic to ensure that no changes are made without the user knowing about it, and to cope with supply chain attacks like this one.
That's why users wanting to be 100% sure the same action logic is run every time, use commit hashes like so:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2which is also possible through github-workflows-kt:
@file:DependsOn("actions:checkout:85e6279cec87321a52edac9c87bce653a07cf6c2")However, there are the following problems:
- If an action's typing is stored in the typing catalog, it's not used during binding generation, which in practice
means that all inputs are of type
String. - It's not possible to make it dependency updating bots-friendly. With the YAML approach, one can write:
and the bots will generally suggest updates to the hash and the version in the comment. With github-workflows-kt, not only the bots won't learn about available versions accessible through the commit hashes, but also the comment with the version won't end up in the YAML.
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 # v4.1.2
For better reproducibility, users might want to specify a particular commit in github-actions-typing-catalog. Right now, typings stored in the catalog are versioned by the major versions, so e.g. if a typing change is required because of a change in the action between v1.0 and v1.1, it may be a breaking change for the Kotlin binding consumer.
To solve it, we could provide a way to freeze not only the action's commit hash, but also the catalog typing's commit hash. However, this problem is neglected for the purpose of this document because demand for this feature is unknown (there is no signal about it from the users), and it would lead to further complicating the system.
One could imagine an attack on the bindings server where a malicious piece of code is injected to the Kotlin-based logic. To address it, we could have a way of specifying e.g. a checksum of the binding's JAR, to be sure that the delivered binding is always the same.
However, there's no native support for dependency checksums in Kotlin Scripting, and implementing it in an acceptable way from user's point of view is not possible. It would be also challenging to ensure that the JAR provided by the server doesn't change - in practice, it's not possible as even a subtle change e.g. in tooling used to build the JAR may lead to changing the checksum.
Recommended workarounds are
- private hosting of the bindings server, either by building it from source (then one can pin the repo's commit) or pinning to an image's digest in DockerHub
- fetching the binding to a static location, and consuming it from the script. However, this way makes using dependency updating bots extremely hard
Add two new ways of consuming an action binding:
Example:
@file:Repository("https://bindings.krzeminski.it")
@file:DependsOn("actions:checkout___commit:v4.1.2__85e6279cec87321a52edac9c87bce653a07cf6c2")which would end up in the YAML as:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 # v4.1.2Additionally, the bindings server would validate if the version ref (here: v4.1.2) points to the mentioned commit
hash. Thanks to the extra validation, the user has an extra assurance than with the YAML approach - the version is for sure
correct.
Since the version is known and validated, the server can also look up the typing in the catalog.
Example:
@file:Repository("https://bindings.krzeminski.it")
@file:DependsOn("actions:checkout___commit_lenient:v4.1.2__85e6279cec87321a52edac9c87bce653a07cf6c2")which also would end up in the YAML as:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 # v4.1.2It's similar to the above approach, with just one difference: the version is just used to add a comment in the YAML - no validation like described in the previous approach is performed.
This mode is created to closer resemble how the YAML approach works. Because of no extra validation, it allows handling certain edge cases, when the commit hash is intentionally out of sync with the version. For example:
- The version tag was deleted because of a security vulnerability, and the user prefers to keep the action usage pinned to the commit hash to keep the workflow working, as opposed to failing in the consistency check job.
- The user wants to use a newer hash with some fix that did not make it into a release yet.
This mode is made more verbose in the code on purpose, to promote the first, safer approach.