feat: add wings_enabled input (mise-wings cache integration)

Adds a single new input — `wings_enabled` — that gates the
[mise-wings](https://mise-wings.en.dev) asset cache for tool
installs. Existing workflows are unaffected: default `false`
is a no-op.

| Input | Default | Description |
|---|---|---|
| `wings_enabled` | `false` | Route tool-install URLs through the wings cache when `true` |

## How it works

When `wings_enabled: true`, the action exports
`MISE_WINGS_ENABLED=1`. Authentication is fully automatic —
mise itself owns the GHA OIDC → wings session exchange. No
`mise wings login` step in workflow YAML, no long-lived
secrets to rotate.

When mise (built with wings support — see jdx/mise#9458)
sees `MISE_WINGS_ENABLED=1` and detects the GHA OIDC env
vars (`ACTIONS_ID_TOKEN_REQUEST_URL` +
`ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it:

  1. Fetches the runner's OIDC token, scoped to the wings
     deployment audience
  2. POSTs it to `https://api.<host>/auth` to mint a wings
     CI session JWT
  3. Caches the JWT in-process for the rest of the workflow
  4. Transparently rewrites `registry.npmjs.org` /
     `github.com` / `api.github.com` URLs to the wings
     cache subdomains and attaches the JWT as a Bearer
     header

## Why opt-in (not opt-out)

The default-off posture is deliberate. Many workflows
already declare `permissions: id-token: write` for unrelated
reasons (SLSA provenance, AWS OIDC, Sigstore, npm
provenance). If `wings_enabled` defaulted to `true`, those
workflows would silently send the runner's OIDC identity
claims to a third-party cache without explicit consent.
Cursor Bugbot HIGH + Greptile P1+security flagged a prior
"default true" iteration of this PR as a privacy
regression.

Explicit opt-in keeps the gate visible in the workflow YAML.

## Workflow requirements

```yaml
permissions:
  id-token: write   # required for OIDC

jobs:
  build:
    steps:
      - uses: jdx/mise-action@<sha>
        with:
          wings_enabled: true
```

The action emits a clear warning when `wings_enabled: true`
but `id-token: write` is missing — without that hint, the
user would see "wings configured but doing nothing" and have
no clue why.

## Notes

- Older mise binaries see `MISE_WINGS_ENABLED` and silently
  ignore it — forward-compatible.
- `setupMise` fetches the mise binary itself with `curl`,
  which doesn't go through mise's HTTP layer; the wings
  rewriter only kicks in once the resulting mise binary
  runs `mise install`. The action sets the env var before
  any `mise` subcommand runs.
This commit is contained in:
jdx 2026-04-29 08:15:17 -05:00
parent 0a780158e1
commit 969042fe52
No known key found for this signature in database
GPG key ID: 584DADE86724B407
4 changed files with 148 additions and 1 deletions

View file

@ -85,6 +85,36 @@ inputs:
description: "Automatically load mise env vars into GITHUB_ENV. Note that PATH modifications are not part of this."
required: false
default: "true"
wings_enabled:
description: |
[experimental] Opt in to the mise-wings asset cache
(https://mise-wings.en.dev) for this action invocation.
When `true`, the action exports `MISE_WINGS_ENABLED=1` so
the installed mise binary routes tool-install URLs (npm
tarballs, GitHub release artifacts) through the per-org
wings cache subdomains.
Authentication is automatic via the runner's GitHub OIDC
identity — no `mise wings login` step, no long-lived
secret to rotate. The workflow must declare
`permissions: id-token: write` so the OIDC token-issuer
env vars are populated; without that, mise falls through
to direct-origin fetches transparently.
Default `false` is the conservative posture: a workflow
with `id-token: write` (used for SLSA / AWS-OIDC /
Sigstore / etc.) should not have its OIDC token sent to
a third-party cache without explicit opt-in. Older mise
binaries that don't speak wings ignore the env var
entirely, so this is forward-compatible.
Requires an active mise-wings subscription on the Clerk
org linked to the GitHub org running the workflow;
without one, the proxy 402s and mise leaves the cache
off without affecting the workflow's success.
required: false
default: "false"
outputs:
cache-hit:
description: A boolean value to indicate if a cache was hit.

55
dist/index.js generated vendored
View file

@ -85677,6 +85677,24 @@ async function run() {
else {
setOutput('cache-hit', false);
}
// Wings opt-in hook (experimental). When
// `wings_enabled: true` is set, this exports
// `MISE_WINGS_ENABLED=1` so subsequent `mise install`
// commands in this workflow route through the wings
// cache. Default `false` so workflows with
// `id-token: write` (used for SLSA / AWS-OIDC / Sigstore /
// etc.) don't silently send the runner's OIDC token to
// a third-party cache without explicit consent.
//
// Note: `setupMise` fetches the mise binary itself with
// `curl`, which doesn't go through mise's HTTP layer —
// the wings rewriter only kicks in once the resulting
// mise binary runs `mise install` and friends. Ordering
// here is irrelevant for binary acceleration; we just
// want the env var set before any `mise` subcommand
// runs. Greptile + Gemini both flagged the previous
// comment as overstating what the early call accelerates.
setupWings();
const version = getInput('version');
const fetchFromGitHub = getBooleanInput('fetch_from_github');
await setupMise(version, fetchFromGitHub);
@ -85704,6 +85722,43 @@ async function run() {
throw err;
}
}
/**
* Opt in to mise-wings caching for this workflow run. When
* `wings_enabled: true`, exports `MISE_WINGS_ENABLED=1` so
* subsequent `mise install` commands route through the
* cache.
*
* Mise itself owns the OIDC wings session exchange when
* it sees `MISE_WINGS_ENABLED=1` and the GHA OIDC env vars
* (`ACTIONS_ID_TOKEN_REQUEST_URL` +
* `ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it fetches the runner's
* OIDC token, exchanges it at the proxy's `POST /auth`
* route, and caches the resulting session JWT for the rest
* of the process.
*
* Pre-flight check: `id-token: write` permission must be
* declared at the workflow or job level for the OIDC env
* vars to be present. We log a warning when wings is
* enabled but the env vars are absent without this hint,
* the user sees a transparent "wings configured but doing
* nothing" which is hard to debug.
*/
function setupWings() {
if (!getBooleanInput('wings_enabled')) {
return;
}
exportVariable('MISE_WINGS_ENABLED', '1');
info("mise-wings: enabled. mise will exchange the runner's OIDC token for a wings session on first use.");
const oidcUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const oidcToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!oidcUrl || !oidcToken) {
warning('mise-wings: GHA OIDC env vars are missing. Add ' +
'`permissions: id-token: write` at the workflow or job ' +
'level so the runner can mint OIDC tokens. Without this, ' +
'mise falls through to direct-origin fetches and the cache ' +
'is bypassed.');
}
}
async function exportMiseEnv() {
startGroup('Exporting mise environment variables');
const cwd = getCwd();

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View file

@ -54,6 +54,25 @@ async function run(): Promise<void> {
core.setOutput('cache-hit', false)
}
// Wings opt-in hook (experimental). When
// `wings_enabled: true` is set, this exports
// `MISE_WINGS_ENABLED=1` so subsequent `mise install`
// commands in this workflow route through the wings
// cache. Default `false` so workflows with
// `id-token: write` (used for SLSA / AWS-OIDC / Sigstore /
// etc.) don't silently send the runner's OIDC token to
// a third-party cache without explicit consent.
//
// Note: `setupMise` fetches the mise binary itself with
// `curl`, which doesn't go through mise's HTTP layer —
// the wings rewriter only kicks in once the resulting
// mise binary runs `mise install` and friends. Ordering
// here is irrelevant for binary acceleration; we just
// want the env var set before any `mise` subcommand
// runs. Greptile + Gemini both flagged the previous
// comment as overstating what the early call accelerates.
setupWings()
const version = core.getInput('version')
const fetchFromGitHub = core.getBooleanInput('fetch_from_github')
await setupMise(version, fetchFromGitHub)
@ -79,6 +98,49 @@ async function run(): Promise<void> {
}
}
/**
* Opt in to mise-wings caching for this workflow run. When
* `wings_enabled: true`, exports `MISE_WINGS_ENABLED=1` so
* subsequent `mise install` commands route through the
* cache.
*
* Mise itself owns the OIDC wings session exchange when
* it sees `MISE_WINGS_ENABLED=1` and the GHA OIDC env vars
* (`ACTIONS_ID_TOKEN_REQUEST_URL` +
* `ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it fetches the runner's
* OIDC token, exchanges it at the proxy's `POST /auth`
* route, and caches the resulting session JWT for the rest
* of the process.
*
* Pre-flight check: `id-token: write` permission must be
* declared at the workflow or job level for the OIDC env
* vars to be present. We log a warning when wings is
* enabled but the env vars are absent without this hint,
* the user sees a transparent "wings configured but doing
* nothing" which is hard to debug.
*/
function setupWings(): void {
if (!core.getBooleanInput('wings_enabled')) {
return
}
core.exportVariable('MISE_WINGS_ENABLED', '1')
core.info(
"mise-wings: enabled. mise will exchange the runner's OIDC token for a wings session on first use."
)
const oidcUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL
const oidcToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
if (!oidcUrl || !oidcToken) {
core.warning(
'mise-wings: GHA OIDC env vars are missing. Add ' +
'`permissions: id-token: write` at the workflow or job ' +
'level so the runner can mint OIDC tokens. Without this, ' +
'mise falls through to direct-origin fetches and the cache ' +
'is bypassed.'
)
}
}
async function exportMiseEnv(): Promise<void> {
core.startGroup('Exporting mise environment variables')