axodotdev / axodotdev/cargo-dist
Support for actions as custom "jobs"
- Dominant language
- Rust
- Stars
- 2.1k
- Forks
- 149
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 32
Description
Hello!
Currently, `dist` has support for custom jobs via reusable workflows, per the docs: https://axodotdev.github.io/cargo-dist/book/ci/customizing.html#custom-jobs
Specifically, if a user does:
```toml
publish-jobs = ["./publish-pypi"]
```
then `release.yml` might contain something like:
```yaml
custom-publish-pypi:
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/publish-pypi.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
"id-token": "write"
"packages": "write"
```
...where `publish-pypi.yml` is a custom reusable workflow.
## Proposal
I'd like to propose extending the above to include support for custom *actions*, in addition to the current reusable workflow support.
Here's a rough idea of what that could look like:
```toml
publish-jobs = [
{ action = "./.github/actions/publish-pypi" }
]
```
which would produce, roughly:
```yaml
custom-publish-pypi:
runs-on: ubuntu-latest # defaults to the global runner
permissions:
id-token: write
packages: write
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
steps:
- uses: ./.github/actions/publish-pypi
with:
plan: ${{ needs.plan.outputs.val }}
```
In effect, this would be pretty similar to the generation for reusable workflows, except that it'd be a "normal" job definition with a single action call instead of a reusable workflow call.
## Motivation
My primary motivation for this is working around some weirdness/inconsistencies between normal workflows and reusable workflows, specifically in the context of things like Trusted Publishing and (PEP 740) attestations.
Specifically:
* GitHub distinguishes between "top" and "bottom" level workflows via the `workflow_ref` and `job_workflow_ref` claims in OIDC. By default (non-reusable contexts) these are the same, but in reusable contexts the "top" is the _caller_ and the "bottom" is the _lastmost callee_ (i.e., the last reusable workflow in a chain of potentially multiple).
* The divergence between these two claims in a reusable workflow context presents a challenge for PyPI and other implementors of Trusted Publishing, since it's not clear which is the "right" identity for upload authorization purposes. In practice PyPI does a slightly wonky thing of checking `job_workflow_ref` and cross-checking it against the top-level repository, which makes reusable workflows work but only if they're actually called from their source origin.
* That wonky behavior in turn has weird consequences for things like Sigstore-based attestations, since attestations are signed by an identity that reflects both the "top" and "bottom" workflows.
The TL;DR of all of this is that reusable workflows complicate the notion of "identity" in a way that's not super easy for downstreams that rely on workflow identity (like package indices/attestation verifiers) to disentangle. Some of that is for bad (implementation error) reasons, some of it is because the relationship between normal/reusable jobs is subtle and not hard to model (e.g. what trust should be assigned to a reusable workflow called from _outside_ its source repository)?
Custom actions sidestep these complications, because they run directly within the workflow (and job) they're invoked in. As such, they have the same workflow "identity" as their caller.
## Other considerations
For an MVP, I think it probably makes sense to allow local actions only. In other words, `action` should always start with `./`, not be something external. That could probably be broadened, but initially it would be similar to the support for reusable workflows (which seems limited to local ones?)
Because this involves "normal" jobs rather than reusable ones, there _might_ be some others things that would be worth allowing configuration for. For example:
```toml
publish-jobs = [
{
action = "./.github/actions/publish-pypi",
runner = "my-custom-runner",
}
]
```
This might not be needed for an MVP, however.
## Other notes
If this sounds interesting/worth supporting, I'd be happy to send a PR for it 🙂
Contributor guide
Research direction
Start with the custom-jobs documentation and the existing reusable-workflow generation that produces release.yml. Compare its behavior with the proposed local action job using publish-pypi examples, then define the MVP boundary and verify that a local action configuration generates the expected normal job with its inputs and permissions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github-actions, rust
- Domain
- ci-cd, devops, release
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100