Compare commits

..

No commits in common. "main" and "v1.2.5" have entirely different histories.
main ... v1.2.5

48 changed files with 141174 additions and 91126 deletions

4
.eslintignore Normal file
View file

@ -0,0 +1,4 @@
lib/
dist/
node_modules/
coverage/

55
.eslintrc.json Normal file
View file

@ -0,0 +1,55 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"i18n-text/no-en": "off",
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"camelcase": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/semi": ["error", "never"],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}

View file

@ -1,39 +0,0 @@
env:
node: true
es6: true
jest: true
globals:
Atomics: readonly
SharedArrayBuffer: readonly
ignorePatterns:
- '!.*'
- '**/node_modules/.*'
- '**/dist/.*'
- '**/coverage/.*'
- '*.json'
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2023
sourceType: module
project:
- './.github/linters/tsconfig.json'
- './tsconfig.json'
plugins:
- jest
- '@typescript-eslint'
extends:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended
- plugin:github/recommended
- plugin:jest/recommended
rules:
'i18n-text/no-en': off
'import/no-namespace': off

11
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: npm
directory: /
schedule:
interval: weekly

83
.github/linters/.eslintrc.yml vendored Normal file
View file

@ -0,0 +1,83 @@
env:
node: true
es6: true
jest: true
globals:
Atomics: readonly
SharedArrayBuffer: readonly
ignorePatterns:
- '!.*'
- '**/node_modules/.*'
- '**/dist/.*'
- '**/coverage/.*'
- '*.json'
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2023
sourceType: module
project:
- './.github/linters/tsconfig.json'
- './tsconfig.json'
plugins:
- jest
- '@typescript-eslint'
extends:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended
- plugin:github/recommended
- plugin:jest/recommended
rules:
{
'camelcase': 'off',
'eslint-comments/no-use': 'off',
'eslint-comments/no-unused-disable': 'off',
'i18n-text/no-en': 'off',
'import/no-namespace': 'off',
'no-console': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error',
'semi': 'off',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/explicit-member-accessibility':
['error', { 'accessibility': 'no-public' }],
'@typescript-eslint/explicit-function-return-type':
['error', { 'allowExpressions': true }],
'@typescript-eslint/func-call-spacing': ['error', 'never'],
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-for-in-array': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-var-requires': 'error',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-function-type': 'warn',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/restrict-plus-operands': 'error',
'@typescript-eslint/semi': ['error', 'never'],
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/unbound-method': 'error'
}

7
.github/linters/.markdown-lint.yml vendored Normal file
View file

@ -0,0 +1,7 @@
# Unordered list style
MD004:
style: dash
# Ordered list item prefix
MD029:
style: one

10
.github/linters/.yaml-lint.yml vendored Normal file
View file

@ -0,0 +1,10 @@
rules:
document-end: disable
document-start:
level: warning
present: false
line-length:
level: warning
max: 80
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: true

View file

@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>jdx/renovate-config"],
"gitIgnoredAuthors": ["autofix.ci[bot] <autofix.ci[bot]@users.noreply.github.com>"],
"rebaseWhen": "conflicted"
}

View file

@ -12,13 +12,13 @@ on:
push: push:
branches: branches:
- main - main
paths-ignore:
- '**.md'
pull_request: pull_request:
paths-ignore:
- '**.md'
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
jobs: jobs:
check-dist: check-dist:
name: Check dist/ name: Check dist/
@ -31,19 +31,21 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with:
persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- name: Install Dependencies - name: Install Dependencies
id: install id: install
run: aube ci run: npm ci
- name: Build dist/ Directory - name: Build dist/ Directory
id: build id: build
run: aubr bundle run: npm run bundle
- name: Compare Expected and Actual Directories - name: Compare Expected and Actual Directories
id: diff id: diff
@ -56,7 +58,7 @@ jobs:
# If index.js was different than expected, upload the expected version as # If index.js was different than expected, upload the expected version as
# a workflow artifact. # a workflow artifact.
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - uses: actions/upload-artifact@v3
if: ${{ failure() && steps.diff.conclusion == 'failure' }} if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with: with:
name: dist name: dist

View file

@ -7,13 +7,6 @@ on:
- main - main
- 'releases/*' - 'releases/*'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
jobs: jobs:
test-typescript: test-typescript:
name: TypeScript Tests name: TypeScript Tests
@ -22,31 +15,26 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with:
persist-credentials: false
# `mise.toml` pins both Node and aube; mise-action installs - name: Setup Node.js
# whatever's listed there. Reads `package-lock.json` id: setup-node
# directly — no separate `aube-lock.yaml` to maintain. uses: actions/setup-node@v3
# `.npmrc` pins `node-linker=hoisted` so the layout is
# npm-flat (rollup's `--configPlugin` resolution
# requires this).
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with: with:
cache: false node-version: 18
cache: npm
- name: Install Dependencies - name: Install Dependencies
id: aube-ci id: npm-ci
run: aube ci run: npm ci
- name: Check Format - name: Check Format
id: aube-format-check id: npm-format-check
run: aubr format:check run: npm run format:check
- name: Lint - name: Lint
id: aube-lint id: npm-lint
run: aubr lint run: npm run lint
# - name: Test # - name: Test
# id: npm-ci-test # id: npm-ci-test

View file

@ -10,10 +10,6 @@ on:
schedule: schedule:
- cron: '31 7 * * 3' - cron: '31 7 * * 3'
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@ -34,21 +30,19 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
id: initialize id: initialize
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
source-root: src source-root: src
- name: Autobuild - name: Autobuild
id: autobuild id: autobuild
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
id: analyze id: analyze
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/analyze@v2

45
.github/workflows/linter.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: Lint Code Base
on:
pull_request:
branches:
- main
push:
branches-ignore:
- main
jobs:
lint:
name: Lint Code Base
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
statuses: write
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- name: Install Dependencies
id: install
run: npm ci
- name: Lint Code Base
id: super-linter
uses: super-linter/super-linter/slim@v5
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TYPESCRIPT_DEFAULT_STYLE: prettier
VALIDATE_JSCPD: false
VALIDATE_NATURAL_LANGUAGE: false

View file

@ -1,21 +0,0 @@
name: pr-closer
on:
schedule:
- cron: "0 0 * * *" # daily at midnight
workflow_dispatch:
concurrency:
group: pr-closer
cancel-in-progress: true
jobs:
pr-closer:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
checks: read
statuses: read
steps:
- uses: jdx/pr-closer@44b9e7859d29e01b4b38e50e2ad6b5c5d00a6973 # v1.0.1

View file

@ -1,34 +0,0 @@
name: release-plz
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
push:
branches:
- main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_FUND: false
concurrency:
group: release-plz
jobs:
release-plz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
submodules: recursive
token: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }}
persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- run: mise run release-plz
env:
DRY_RUN: 0
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }}

View file

@ -1,49 +0,0 @@
name: Release
on:
pull_request:
types: [closed]
branches: [main]
permissions: {}
jobs:
release:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
persist-credentials: false
- name: Setup mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- name: Release
run: ./scripts/postversion.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
enhance-release:
needs: [release]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- name: Enhance release notes with communique
run: |
TAG_NAME="v$(jq -r .version package.json)"
communique generate "$TAG_NAME" --github-release
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }}

View file

@ -1,77 +0,0 @@
name: Test Redacted Environment Variables
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
test-redacted-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Create test mise config with sensitive values
run: |
cat > .mise.toml << 'EOF'
[env]
PUBLIC_VAR = "this-is-public"
API_KEY = {value = "secret-api-key-12345", redact = true}
SECRET_TOKEN = {value = "supersecret-token-xyz", redact = true}
DATABASE_PASSWORD = {value = "db-pass-789", redact = true}
EOF
- name: Setup mise
uses: ./
- name: Verify environment variables are exported
run: |
echo "Checking if environment variables are set..."
# Check that public var is set
if [ "$PUBLIC_VAR" != "this-is-public" ]; then
echo "ERROR: PUBLIC_VAR not set correctly"
exit 1
fi
echo "✓ PUBLIC_VAR is set correctly"
# Check that sensitive vars are set (but their values should be masked in logs)
if [ -z "$API_KEY" ]; then
echo "ERROR: API_KEY not set"
exit 1
fi
echo "✓ API_KEY is set"
if [ -z "$SECRET_TOKEN" ]; then
echo "ERROR: SECRET_TOKEN not set"
exit 1
fi
echo "✓ SECRET_TOKEN is set"
if [ -z "$DATABASE_PASSWORD" ]; then
echo "ERROR: DATABASE_PASSWORD not set"
exit 1
fi
echo "✓ DATABASE_PASSWORD is set"
- name: Test that sensitive values are masked (will show *** if properly masked)
run: |
echo "Testing value masking..."
echo "API_KEY value: $API_KEY"
echo "SECRET_TOKEN value: $SECRET_TOKEN"
echo "DATABASE_PASSWORD value: $DATABASE_PASSWORD"
echo "PUBLIC_VAR value: $PUBLIC_VAR"
# This should show the actual values in the step output,
# but GitHub Actions should mask them if core.setSecret was called
- name: Verify mise version
run: mise --version

View file

@ -1,191 +1,42 @@
name: "build-test" name: 'build-test'
on: # rebuild any PRs and main branch changes on: # rebuild any PRs and main branch changes
pull_request: pull_request:
branches:
- main
push: push:
branches: branches:
- main - main
- 'releases/*'
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
jobs: jobs:
build: # make sure build/ci work properly build: # make sure build/ci work properly
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: - run: |
persist-credentials: false npm install
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - run: |
- run: aube install npm run all
- run: aubr all
test: # make sure the action works on a clean machine without building test: # make sure the action works on a clean machine without building
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: os:
- name: ubuntu - ubuntu-latest
runs-on: ubuntu-latest - macos-latest
- name: macos tool_versions:
runs-on: macos-latest - |
- name: windows nodejs 20
runs-on: windows-latest - |
- name: alpine nodejs 18
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
container: alpine:3.22@sha256:310c62b5e7ca5b08167e4384c68db0fd2905dd9c7493756d356e893909057601
requirements: apk add --no-cache curl bash
name: ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }}
container: ${{ matrix.container }}
steps: steps:
- name: Install requirements - uses: actions/checkout@v4
if: ${{ matrix.requirements }} - uses: ./
run: ${{ matrix.requirements }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false tool_versions: ${{ matrix.tool_versions }}
- name: Setup mise
uses: ./
with:
mise_toml: |
[tools]
jq = "1.7.1"
[env]
MY_ENV_VAR = "abc"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: mise --version - run: rtx --version
- run: mise x jq -- jq --version - run: rtx exec -- node --version
- run: which jq - run: which node
- run: jq --version - run: node -v
- name: Check Windows shim binary
if: runner.os == 'Windows'
shell: pwsh
run: |
$miseBinDir = Split-Path -Parent (Get-Command mise).Source
$miseShim = Join-Path $miseBinDir "mise-shim.exe"
if (!(Test-Path -LiteralPath $miseShim)) {
throw "mise-shim.exe was not installed next to mise.exe"
}
- run: . scripts/test.sh
shell: bash
specific_version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup mise
uses: ./
with:
cache_save: ${{ github.ref_name == 'main' }}
cache_key_prefix: mise-v1
version: 2025.7.3
sha256: d38d4993c5379a680b50661f86287731dc1d1264777880a79b786403af337948
install_args: bun
mise_toml: |
[tools]
bun = "1"
- run: which bun
- run: bun -v
- name: Update mise
uses: ./
with:
cache_save: ${{ github.ref_name == 'main' }}
cache_key_prefix: mise-v1
version: v2025.7.3 # should trim the `v`
sha256: d38d4993c5379a680b50661f86287731dc1d1264777880a79b786403af337948
checksum_failure:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup mise
id: bad
uses: ./
with:
version: 2024.9.6
sha256: 1f0b8c3d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t
continue-on-error: true
- name: Dump steps context
if: ${{ always() }}
env:
STEPS_CONTEXT: ${{ toJson(steps) }}
run: echo "$STEPS_CONTEXT"
- name: expect failure
run: echo "Failed as expected"
if: ${{ steps.bad.outcome == 'failure' }}
- name: not failed as expected
run: |
echo "Expected failure but the job was ${STEPS_BAD_OUTCOME}"
exit 1
if: ${{ steps.bad.outcome != 'failure' }}
env:
STEPS_BAD_OUTCOME: ${{ steps.bad.outcome }}
custom_cache_key:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup mise with custom cache key
uses: ./
with:
cache_key: "custom-{{platform}}-{{install_args_hash}}-${{ github.run_id }}"
install_args: "jq@1.7.1"
mise_toml: |
[tools]
jq = "1.7.1"
- run: mise --version
- run: mise x jq -- jq --version
- run: which jq
- run: jq --version
fetch_from_github:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup mise from mise.jdx.dev
uses: ./
with:
fetch_from_github: true
cache: false
cache_save: false
mise_toml: |
[tools]
jq = "1.7.1"
- run: mise --version
- run: mise x jq -- jq --version
- run: which jq
- run: jq --version
final:
needs:
- build
- test
- specific_version
- checksum_failure
- custom_cache_key
- fetch_from_github
runs-on: ubuntu-latest
timeout-minutes: 1
# Run on success or upstream failure but skip when the workflow is cancelled
# — `always()` would override `cancel-in-progress` and waste a runner.
if: ${{ !cancelled() }}
steps:
- name: Check CI job results
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')
run: exit 1

View file

@ -1,22 +0,0 @@
name: zizmor
on:
push:
branches: [main]
paths: ['.github/workflows/**']
pull_request:
paths: ['.github/workflows/**']
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
with:
advanced-security: false

7
.gitignore vendored
View file

@ -101,10 +101,3 @@ __tests__/runner/*
.idea .idea
.vscode .vscode
*.code-workspace *.code-workspace
# Generated by `aube install` to record build-script approvals.
# We've chosen not to commit our approval state — the build doesn't
# need any package's install scripts to run, and the file gets
# regenerated each install anyway. The harmless "ignored build
# scripts" warning in `aube install` output is the cost.
pnpm-workspace.yaml

View file

@ -1,5 +1,6 @@
#!/usr/bin/env sh
# shellcheck disable=SC1091 # shellcheck disable=SC1091
. "$(dirname -- "$0")/_/husky.sh"
npm ci
npm run all npm run all
git add dist git add dist

11
.npmrc
View file

@ -1,11 +0,0 @@
# Forces a flat npm-style `node_modules/` layout instead of
# aube's default symlink/virtual-store. Required for
# deterministic `dist/index.js.map` source-map paths in CI:
# without flat layout, rollup embeds absolute paths into
# aube's per-user cache dir (`/home/runner/.cache/aube/...`),
# which differ across machines and break the `check-dist`
# workflow's byte-equality check.
#
# npm reads `.npmrc` too but ignores `node-linker` (npm
# always installs flat), so the file is safe for both PMs.
node-linker=hoisted

File diff suppressed because it is too large Load diff

View file

@ -1,63 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a GitHub Action that installs and configures mise, a polyglot runtime manager. The action is written in TypeScript and published to the GitHub Actions marketplace.
## Development Commands
This project uses [aube](https://aube.en.dev) as its package
manager (en.dev's pnpm-compat PM, native Rust). It reads
`package-lock.json` directly — no separate `aube-lock.yaml`.
`mise install` will install the pinned aube version
automatically; you can also use `npm` if you prefer (the
`.npmrc`'s `node-linker=hoisted` pin is aube-specific and
ignored by npm).
```bash
# Install dependencies
aube install
# Build, format, lint, and package
aubr all
# Individual commands
aubr format:write # Format code with Prettier
aubr lint # Run ESLint and format check
aubr package # Bundle with rollup for distribution
# Testing
aubr all # Run full build pipeline
./scripts/test.sh # Integration test script
```
## Architecture
The action follows GitHub's standard TypeScript action structure:
1. **Entry Point**: `src/index.ts` - Main action logic that:
- Downloads and installs mise binary
- Manages caching through GitHub Actions cache
- Configures environment variables (MISE_*, GITHUB_TOKEN)
- Runs mise commands (install, reshim, etc.)
- Exports mise environment variables to GITHUB_ENV
2. **Distribution**: `dist/index.js` - Compiled and bundled output (must be committed)
3. **Action Definition**: `action.yml` - Defines inputs, outputs, and metadata
## Key Implementation Details
- **Cache Management**: Uses content-addressable caching based on mise config files (.mise.toml, .tool-versions, etc.)
- **Binary Download**: Supports downloading from GitHub releases or mise.jdx.dev
- **Platform Support**: Handles Linux (glibc/musl), macOS, and Windows
- **Environment Setup**: Automatically adds mise bin and shims directories to PATH
- **GitHub API**: Uses GITHUB_TOKEN to avoid rate limits when installing GitHub-hosted tools
## Important Notes
- Always run `aubr all` before committing to ensure dist/ is updated
- The dist/ folder must be committed as GitHub Actions runs the compiled code
- Test changes using the action itself (uses: ./) in test workflows

119
README.md
View file

@ -13,126 +13,17 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- uses: jdx/mise-action@v4 - uses: jdx/rtx-action@v1
with: with:
version: 2026.3.10 # [default: latest] mise version to install
install: true # [default: true] run `mise install`
install_args: "bun" # [default: ""] additional arguments to `mise install`
cache: true # [default: true] cache mise using GitHub's cache
experimental: true # [default: false] enable experimental features
log_level: debug # [default: info] log level
# automatically write this .tool-versions file
tool_versions: | tool_versions: |
shellcheck 0.11.0 shellcheck 0.9.0
# or, if you prefer .mise.toml format:
mise_toml: |
[tools]
shellcheck = "0.11.0"
working_directory: app # [default: .] directory to run mise in
reshim: false # [default: false] run `mise reshim -f`
github_token: ${{ secrets.GITHUB_TOKEN }} # [default: ${{ github.token }}] GitHub token for API authentication
- run: shellcheck scripts/*.sh - run: shellcheck scripts/*.sh
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- uses: jdx/mise-action@v4 - uses: jdx/rtx-action@v1
# .tool-versions will be read from repo root # .tool-versions will be read from repo root
- run: node ./my_app.js - run: node ./my_app.js
``` ```
## Cache Configuration
You can customize the cache key used by the action:
```yaml
- uses: jdx/mise-action@v4
with:
cache_key: "my-custom-cache-key" # Override the entire cache key
cache_key_prefix: "mise-v1" # Or just change the prefix (default: "mise-v0")
```
### Template Variables in Cache Keys
When using `cache_key`, you can use template variables to reference internal values:
```yaml
- uses: jdx/mise-action@v4
with:
cache_key: "mise-{{platform}}-{{version}}-{{file_hash}}"
version: "2026.3.10"
install_args: "node python"
```
Available template variables:
- `{{version}}` - The mise version (from the `version` input)
- `{{cache_key_prefix}}` - The cache key prefix (from `cache_key_prefix` input or default)
- `{{platform}}` - The target platform, including the runner image (e.g., "linux-x64-ubuntu24", "macos-arm64-macos15", "linux-x64-self-hosted"). The trailing segment is `process.env.ImageOS` on github-hosted runners and falls back to `"self-hosted"` elsewhere — preventing cache collisions when the same repo runs on different runner providers (github-hosted, namespace.so, self-hosted).
- `{{file_hash}}` - Hash of all mise configuration files
- `{{mise_env}}` - The MISE_ENV environment variable value
- `{{install_args_hash}}` - SHA256 hash of the sorted tools from install args
- `{{default}}` - The processed default cache key (useful for extending)
Conditional logic is also supported using Handlebars syntax like `{{#if version}}...{{/if}}`.
Example using multiple variables:
```yaml
- uses: jdx/mise-action@v4
with:
cache_key: "mise-v1-{{platform}}-{{install_args_hash}}-{{file_hash}}"
install_args: "node@24 python@3.14"
```
You can also extend the default cache key:
```yaml
- uses: jdx/mise-action@v4
with:
cache_key: "{{default}}-custom-suffix"
install_args: "node@24 python@3.14"
```
This gives you full control over cache invalidation based on the specific aspects that matter to your workflow.
### Rust Cache
Rust has a known cache interaction because mise installs Rust through `rustup`.
See [jdx/mise-action#215](https://github.com/jdx/mise-action/issues/215).
## GitHub API Rate Limits
When installing tools hosted on GitHub (like `gh`, `node`, `bun`, etc.), mise needs to make API calls to GitHub's releases API. Without authentication, these calls are subject to GitHub's rate limit of 60 requests per hour, which can cause installation failures.
```yaml
- uses: jdx/mise-action@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# your other configuration
```
**Note:** The action automatically uses `${{ github.token }}` as the default, so in most cases you don't need to explicitly provide it. However, if you encounter rate limit errors, make sure the token is being passed correctly.
## Lock Files
If a repo mise lock file such as `mise.lock` is present in the working
directory or one of its parents, this action automatically runs
`mise install --locked`. You can still pass `install_args`; `--locked`
will be added automatically unless you already included it yourself.
This auto-detection is intended for repo-managed config files. If you provide
`mise_toml` or `tool_versions` inputs, the action does not automatically force
locked mode.
## Alternative Installation
Alternatively, mise is easy to use in GitHub Actions even without this:
```yaml
jobs:
build:
steps:
- run: |
curl https://mise.run | sh
echo "$HOME/.local/share/mise/bin" >> $GITHUB_PATH
echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH
```

View file

@ -1,123 +1,22 @@
name: mise action name: rtx action
description: Actions for working with mise runtime manager description: Actions for working with rtx runtime manager
author: Jeff Dickey <@jdx> author: Jeff Dickey <@jdx>
branding: branding:
icon: arrow-down-circle icon: arrow-down-circle
color: purple color: purple
inputs: inputs:
version:
required: false
description: The version of mise to use. If not specified, will use the latest release.
sha256:
required: false
description: The SHA256 checksum of the mise binary to verify the download.
mise_dir:
required: false
description: |
The directory that mise will be installed to, defaults to $HOME/.local/share/mise
Or $XDG_DATA_HOME/mise if $XDG_DATA_HOME is set.
Or $MISE_DATA_DIR if $MISE_DATA_DIR is set.
tool_versions: tool_versions:
required: false required: false
description: If present, this value will be written to the .tool-versions file description: If present, this value will be written to the .tool-versions file
mise_toml:
required: false
description: If present, this value will be written to the mise.toml file
install: install:
required: false required: false
default: "true" default: "true"
description: if false, will not run `mise install` description: 'if false, will not run `rtx install`'
install_args:
required: false
description: Arguments to pass to `mise install` such as "bun" to only install bun. When a repo mise lock file is present, the action automatically adds `--locked` unless you already provided it.
install_dir:
required: false
description: deprecated
cache:
required: false
default: "true"
description: if false, action will not read or write to cache
cache_save:
required: false
default: "true"
description: if false, action will not write to cache
cache_key_prefix:
required: false
default: "mise-v1"
description: The prefix key to use for the cache, change this to invalidate the cache
cache_key:
required: false
description: |
Override the complete cache key (ignores all other cache key options).
Supports template variables: {{version}}, {{cache_key_prefix}}, {{platform}}, {{file_hash}},
{{mise_env}}, {{install_args_hash}}, {{default}}, {{env.VAR_NAME}} for environment variables,
and conditional logic like {{#if version}}...{{/if}}
experimental:
required: false
default: "false"
description: if true, will use experimental features
log_level:
required: false
default: "info"
description: The log level to use for the action
working_directory:
required: false
description: The directory that mise runs in
reshim:
required: false
default: "false"
description: if true, will run `mise reshim --all` after setting up mise
add_shims_to_path:
required: false
default: "true"
description: if false, will not add mise shims directory to PATH
github_token:
required: false
description: |
GitHub token for API authentication to avoid rate limits when installing GitHub-hosted tools.
Defaults to the automatic GitHub token.
default: ${{ github.token }}
fetch_from_github:
required: false
default: "true"
description: If true (default), fetch the mise binary from GitHub. If false and using the latest version, fetch from mise.jdx.dev instead.
env:
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: outputs:
cache-hit: cache-hit:
description: A boolean value to indicate if a cache was hit. description: 'A boolean value to indicate if a cache was hit.'
runs: runs:
using: node24 using: node20
main: dist/index.js main: dist/index.js
post: dist/cache-save/index.js
post-if: success()

View file

@ -1,129 +0,0 @@
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# changelog header
header = """
# Changelog\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
---
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") -%}
{% if commit.scope -%}
- {{self::commit(commit=commit)}}\
{% endif -%}
{% endfor -%}
{% for commit in commits -%}
{% if commit.scope -%}
{% else -%}
- {{self::commit(commit=commit)}}\
{% endif -%}
{% endfor -%}
{% endfor %}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 -%}
### New Contributors
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]($REPO/pull/{{ contributor.pr_number }})\
{% endif %}\
{% endfor %}
{% endif -%}
{% macro commit(commit) -%}
{% if commit.scope %}**({{commit.scope}})** {% endif -%}
{% if commit.breaking %}**breaking** {% endif -%}
{{ commit.message | split(pat="\n") | first | trim }} by \
{% if commit.remote.username %}[@{{commit.remote.username}}](https://github.com/{{commit.remote.username}})\
{% else %}{{commit.author.name}}{% endif %} in \
{% if commit.remote.pr_number %}[#{{ commit.remote.pr_number }}]($REPO/pull/{{ commit.remote.pr_number }})\
{% else %}[{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})\
{%- endif %}
{% endmacro commit -%}
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing whitespace from the template
trim = true
postprocessors = [
{ pattern = '\$REPO', replace = "https://github.com/jdx/mise-action" },
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = false
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# remove issue numbers from commits
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = '^\d+\.\d+\.\d+$', skip = true },
{ message = '^(chore|fix)\(deps.*\):', skip = true },
{ message = '^feat', group = "<!-- 0 -->🚀 Features" },
{ message = '^fix', group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = '^doc', group = "<!-- 3 -->📚 Documentation" },
{ message = '^perf', group = "<!-- 4 -->⚡ Performance" },
{ message = '^refactor', group = "<!-- 2 -->🚜 Refactor" },
{ message = '^style', group = "<!-- 5 -->🎨 Styling" },
{ message = '^test', group = "<!-- 6 -->🧪 Testing" },
{ message = '^chore\(release\): prepare for', skip = true },
{ message = '^chore\(pr\)', skip = true },
{ message = '^chore\(pull\)', skip = true },
{ message = '^chore: release v\d+\.\d+\.\d+', skip = true },
{ message = '^chore: Release mise-action', skip = true },
{ message = '^chore|^ci', group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = '.*security', group = "<!-- 8 -->🛡️ Security" },
{ message = '^revert', group = "<!-- 9 -->◀️ Revert" },
{ message = '.', group = "<!-- 11 -->🔍 Other Changes" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
tag_pattern = '^v\d+\.\d+\.\d+$'
# regex for skipping tags
#skip_tags = '^v(1|2023|2024\.0)\.'
# regex for ignoring tags
# ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42
[remote.github]
owner = "jdx"
repo = "mise-action"

59051
dist/cache-save/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/cache-save/index.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/cache-save/sourcemap-register.js generated vendored Normal file

File diff suppressed because one or more lines are too long

158510
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

2443
dist/licenses.txt generated vendored

File diff suppressed because it is too large Load diff

1
dist/sourcemap-register.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,15 +0,0 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
/** @type {import('eslint').Linter.Config[]} */
export default [
{files: ["**/*.{js,mjs,cjs,ts}"]},
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist"],
},
];

166
mise.lock
View file

@ -1,166 +0,0 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.aube]]
version = "1.16.1"
backend = "github:endevco/aube"
[tools.aube."platforms.linux-arm64"]
checksum = "sha256:5e1a207be4b83cd1e0186402764b56e5a702c0332f519959c16d02f4cee9e008"
url = "https://github.com/endevco/aube/releases/download/v1.16.1/aube-v1.16.1-aarch64-unknown-linux-gnu.tar.gz"
url_api = "https://api.github.com/repos/endevco/aube/releases/assets/433127937"
provenance = "github-attestations"
[tools.aube."platforms.linux-arm64-musl"]
checksum = "sha256:8c3098765cee83d654176fd2ed5eadd7224cff08d76f0ce551149fe65ee082d8"
url = "https://github.com/endevco/aube/releases/download/v1.16.1/aube-v1.16.1-aarch64-unknown-linux-musl.tar.gz"
url_api = "https://api.github.com/repos/endevco/aube/releases/assets/432794003"
provenance = "github-attestations"
[tools.aube."platforms.linux-x64"]
checksum = "sha256:3f3d183d6a9644391ffa3ac521c010c90cebc4fd6ab208aa9aa552c70986a357"
url = "https://github.com/endevco/aube/releases/download/v1.16.1/aube-v1.16.1-x86_64-unknown-linux-gnu.tar.gz"
url_api = "https://api.github.com/repos/endevco/aube/releases/assets/432797995"
provenance = "github-attestations"
[tools.aube."platforms.linux-x64-musl"]
checksum = "sha256:2ac54a2bc6248c6b1df9c3a160beea6cf3255102337488f5d3b2dc317ea673a0"
url = "https://github.com/endevco/aube/releases/download/v1.16.1/aube-v1.16.1-x86_64-unknown-linux-musl.tar.gz"
url_api = "https://api.github.com/repos/endevco/aube/releases/assets/432798092"
provenance = "github-attestations"
[tools.aube."platforms.macos-arm64"]
checksum = "sha256:3acf85c4373d30800dd739c279bdbecc5f602ee82009498408b82ce99c8e3f72"
url = "https://github.com/endevco/aube/releases/download/v1.16.1/aube-v1.16.1-aarch64-apple-darwin.tar.gz"
url_api = "https://api.github.com/repos/endevco/aube/releases/assets/432824150"
provenance = "github-attestations"
[tools.aube."platforms.windows-x64"]
checksum = "sha256:950fb818925c31dca4e122bae72a6088c92edc8760c8374fbf0470620ce5e1fa"
url = "https://github.com/endevco/aube/releases/download/v1.16.1/aube-v1.16.1-x86_64-pc-windows-msvc.zip"
url_api = "https://api.github.com/repos/endevco/aube/releases/assets/432804595"
provenance = "github-attestations"
[[tools.communique]]
version = "1.1.2"
backend = "github:jdx/communique"
[tools.communique."platforms.linux-arm64"]
checksum = "sha256:7bb0843207fc3d7b5df2a5c0198bb10539cf13a6b247b4adfbf6b302a68f03de"
url = "https://github.com/jdx/communique/releases/download/v1.1.2/communique-aarch64-unknown-linux-gnu.tar.gz"
url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405964161"
[tools.communique."platforms.linux-arm64-musl"]
checksum = "sha256:b663407be77a370c209df40307b82e436f56a6bc23d4e423510d62ac6e1fedf4"
url = "https://github.com/jdx/communique/releases/download/v1.1.2/communique-aarch64-unknown-linux-musl.tar.gz"
url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405964743"
[tools.communique."platforms.linux-x64"]
checksum = "sha256:5e74ead7037f42940c7dba4f6aa4ed968920cbb55a047aa0d291b0c675c65676"
url = "https://github.com/jdx/communique/releases/download/v1.1.2/communique-x86_64-unknown-linux-gnu.tar.gz"
url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405963914"
provenance = "github-attestations"
[tools.communique."platforms.linux-x64-musl"]
checksum = "sha256:01a6a8b49e635a5a209fdaf6c7b2e976374debc2db1c846c033f567fdba0d86c"
url = "https://github.com/jdx/communique/releases/download/v1.1.2/communique-x86_64-unknown-linux-musl.tar.gz"
url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405964691"
[tools.communique."platforms.macos-arm64"]
checksum = "sha256:459993e31a6c4ccbd09882f5679a2bc1ea5d9068701ecefc411a00fb69ce82e6"
url = "https://github.com/jdx/communique/releases/download/v1.1.2/communique-aarch64-apple-darwin.tar.gz"
url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405964098"
[tools.communique."platforms.windows-x64"]
checksum = "sha256:3cc0e880ac2168aed3163223627bbd1eee62e07a9901cb85cb507c6c8927bc93"
url = "https://github.com/jdx/communique/releases/download/v1.1.2/communique-x86_64-pc-windows-msvc.zip"
url_api = "https://api.github.com/repos/jdx/communique/releases/assets/405964430"
[[tools.gh]]
version = "2.92.0"
backend = "aqua:cli/cli"
[tools.gh."platforms.linux-arm64"]
checksum = "sha256:c2248526dd0160c08d3fccca2332c3c1a07c15a78b23978e77735f1b5a18cfee"
url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_arm64.tar.gz"
provenance = "github-attestations"
[tools.gh."platforms.linux-arm64-musl"]
checksum = "sha256:c2248526dd0160c08d3fccca2332c3c1a07c15a78b23978e77735f1b5a18cfee"
url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_arm64.tar.gz"
provenance = "github-attestations"
[tools.gh."platforms.linux-x64"]
checksum = "sha256:b57848131bdf0c229cd35e1f2a51aa718199858b2e728410b37e89a428943ec4"
url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_amd64.tar.gz"
provenance = "github-attestations"
[tools.gh."platforms.linux-x64-musl"]
checksum = "sha256:b57848131bdf0c229cd35e1f2a51aa718199858b2e728410b37e89a428943ec4"
url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_amd64.tar.gz"
provenance = "github-attestations"
[tools.gh."platforms.macos-arm64"]
checksum = "sha256:b11c54f6bd7d15ed6590475079e5b2fcf36f45d3991a80041b29c9d0cc1f1d07"
url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_macOS_arm64.zip"
provenance = "github-attestations"
[tools.gh."platforms.windows-x64"]
checksum = "sha256:b6a8df3c8c6b9c80f290906387673bc4d272840f3789c5650e0e4e6e75522785"
url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_windows_amd64.zip"
provenance = "github-attestations"
[[tools.git-cliff]]
version = "2.13.1"
backend = "aqua:orhun/git-cliff"
[tools.git-cliff."platforms.linux-arm64"]
checksum = "sha256:4054c124b926c117f3fa048939bc8be0a954f29f3b6f367627e8cb22c1971882"
url = "https://github.com/orhun/git-cliff/releases/download/v2.13.1/git-cliff-2.13.1-aarch64-unknown-linux-musl.tar.gz"
[tools.git-cliff."platforms.linux-arm64-musl"]
checksum = "sha256:4054c124b926c117f3fa048939bc8be0a954f29f3b6f367627e8cb22c1971882"
url = "https://github.com/orhun/git-cliff/releases/download/v2.13.1/git-cliff-2.13.1-aarch64-unknown-linux-musl.tar.gz"
[tools.git-cliff."platforms.linux-x64"]
checksum = "sha256:200d2535da6d9703f3bcc8a4d159c3b55eacdb01cf2148c55b3eee9dd04d5249"
url = "https://github.com/orhun/git-cliff/releases/download/v2.13.1/git-cliff-2.13.1-x86_64-unknown-linux-musl.tar.gz"
[tools.git-cliff."platforms.linux-x64-musl"]
checksum = "sha256:200d2535da6d9703f3bcc8a4d159c3b55eacdb01cf2148c55b3eee9dd04d5249"
url = "https://github.com/orhun/git-cliff/releases/download/v2.13.1/git-cliff-2.13.1-x86_64-unknown-linux-musl.tar.gz"
[tools.git-cliff."platforms.macos-arm64"]
checksum = "sha256:21547ae4a0421164070ab75c2522864ea5565858a011fabc5f583061b20f1226"
url = "https://github.com/orhun/git-cliff/releases/download/v2.13.1/git-cliff-2.13.1-aarch64-apple-darwin.tar.gz"
[tools.git-cliff."platforms.windows-x64"]
checksum = "sha256:3ae3a5549e85c7ad5b20192ebcfee4371269deca51255f6f2f2e051c6541f5ca"
url = "https://github.com/orhun/git-cliff/releases/download/v2.13.1/git-cliff-2.13.1-x86_64-pc-windows-msvc.zip"
[[tools.node]]
version = "24.15.0"
backend = "core:node"
[tools.node."platforms.linux-arm64"]
checksum = "sha256:73afc234d558c24919875f51c2d1ea002a2ada4ea6f83601a383869fefa64eed"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-arm64.tar.gz"
[tools.node."platforms.linux-arm64-musl"]
checksum = "sha256:31e98aa960a067da91edffd5d93bc46657b5d2a8029612c359f5f2ac0060152a"
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-arm64-musl.tar.gz"
[tools.node."platforms.linux-x64"]
checksum = "sha256:44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-x64.tar.gz"
[tools.node."platforms.linux-x64-musl"]
checksum = "sha256:f55af5bd489c5347b113ca6594cae00a54b30ba57ac5875324311bfc6f4762e3"
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-x64-musl.tar.gz"
[tools.node."platforms.macos-arm64"]
checksum = "sha256:372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-arm64.tar.gz"
[tools.node."platforms.windows-x64"]
checksum = "sha256:cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-win-x64.zip"

View file

@ -1,14 +0,0 @@
tasks.pre-commit = ["aubr all", "git add dist"]
tasks.test.alias = ["t"]
tasks.test.run = ["aubr all"]
tasks.lint = "aubr lint"
tasks."lint:fix" = "aubr format:write"
tasks.version = "aube version"
tasks.release-plz = "./scripts/release-plz.sh"
[tools]
node = '24'
aube = 'latest'
git-cliff = 'latest'
gh = 'latest'
communique = 'latest'

8724
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,16 @@
{ {
"name": "mise-action", "name": "rtx-action",
"description": "mise tool setup action", "description": "rtx tool setup action",
"version": "4.1.0", "version": "1.1.1",
"author": "jdx", "author": "jdx",
"type": "module",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/jdx/mise-action.git" "url": "git+https://github.com/jdx/rtx-action.git"
}, },
"keywords": [ "keywords": [
"actions", "actions",
"mise", "rtx",
"setup" "setup"
], ],
"exports": { "exports": {
@ -22,46 +21,33 @@
"bundle": "npm run format:write && npm run package", "bundle": "npm run format:write && npm run package",
"format:check": "prettier --check **/*.ts", "format:check": "prettier --check **/*.ts",
"format:write": "prettier --write **/*.ts", "format:write": "prettier --write **/*.ts",
"lint": "eslint . && npm run format:check", "lint": "npx eslint . -c ./.github/linters/.eslintrc.yml",
"package": "rimraf ./dist && rollup --config rollup.config.mjs", "package": "ncc build src/index.ts --license licenses.txt",
"package:watch": "npm run package -- --watch", "package:watch": "npm run package -- --watch",
"version": "./scripts/version.sh", "prepare": "husky install"
"prepare": "husky"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/cache": "^6.0.0", "@actions/cache": "^3.2.2",
"@actions/core": "^3.0.0", "@actions/core": "^1.10.1",
"@actions/exec": "^3.0.0", "@actions/exec": "^1.1.1",
"@actions/glob": "^0.7.0", "@actions/glob": "^0.4.0"
"@actions/io": "^3.0.0",
"@types/handlebars": "^4.0.40",
"handlebars": "^4.7.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@types/node": "^20.9.2",
"@eslint/js": "^10.0.0", "@typescript-eslint/eslint-plugin": "^6.11.0",
"@rollup/plugin-commonjs": "^29.0.0", "@typescript-eslint/parser": "^6.12.0",
"@rollup/plugin-json": "^6.1.0", "@vercel/ncc": "^0.38.1",
"@rollup/plugin-node-resolve": "^16.0.0", "eslint": "^8.54.0",
"@rollup/plugin-typescript": "^12.0.0", "eslint-plugin-github": "^4.10.1",
"@types/eslint__js": "^8.42.3", "eslint-plugin-jest": "^27.6.0",
"@types/node": "^24", "eslint-plugin-jsonc": "^2.10.0",
"eslint": "^10.0.0", "eslint-plugin-prettier": "^5.0.1",
"globals": "^17.0.0", "husky": "^8.0.3",
"husky": "^9.1.7", "jest": "^29.7.0",
"jest": "^30",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"prettier": "^3.4.1", "prettier": "^3.1.0",
"rimraf": "^6.0.0", "prettier-eslint": "^16.1.2",
"rollup": "^4.0.0", "typescript": "^5.2.2"
"rollup-plugin-license": "^3.7.1",
"typescript": "^6.0.0",
"typescript-eslint": "^8.16.0"
},
"aube": {
"allowBuilds": {
"unrs-resolver": false
}
} }
} }

View file

@ -1,29 +0,0 @@
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import nodeResolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import license from 'rollup-plugin-license'
import path from 'path'
const config = {
input: 'src/index.ts',
output: {
esModule: true,
file: 'dist/index.js',
format: 'es',
sourcemap: true
},
plugins: [
typescript(),
nodeResolve({ preferBuiltins: true }),
commonjs({ ignoreTryCatch: false }),
json(),
license({
thirdParty: {
output: path.resolve('dist', 'licenses.txt')
}
})
]
}
export default config

View file

@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
VERSION=$(jq -r .version package.json)
MAJOR_VERSION=$(echo "$VERSION" | cut -d. -f1)
# Configure git to use gh's credential helper. The checkout step uses
# persist-credentials: false (per zizmor's artipacked audit), so the
# token isn't written to .git/config and raw `git push` would 403.
gh auth setup-git
# create the version tag (allow it to fail if it already exists)
git tag "v$VERSION" || echo "Tag v$VERSION already exists locally"
# push changes to github
git push
# push the current tag to github
git push origin "v$VERSION" || echo "Tag v$VERSION already exists on remote"
# set the major version tag to this release
git tag "v$MAJOR_VERSION" -f
# push the major version tag to github (retry with pull if it fails)
if ! git push origin "v$MAJOR_VERSION" -f; then
echo "Failed to push v$MAJOR_VERSION tag, pulling and retrying..."
git fetch origin "refs/tags/v$MAJOR_VERSION:refs/tags/v$MAJOR_VERSION" -f
git tag "v$MAJOR_VERSION" -f
git push origin "v$MAJOR_VERSION" -f
fi
# check if release already exists before creating
if gh release view "v$VERSION" >/dev/null 2>&1; then
echo "Release v$VERSION already exists, skipping creation"
else
# create a release on github
gh release create "v$VERSION" --generate-notes --verify-tag
fi

View file

@ -1,84 +0,0 @@
#!/usr/bin/env bash
# shellcheck shell=bash
set -euxo pipefail
# Get the current package.json version before any modifications
cur_pkg_version="$(jq -r .version package.json)"
# Get the latest GitHub release version
latest_release="$(gh release view --json tagName --jq .tagName 2>/dev/null || echo "")"
latest_release_version="${latest_release#v}"
# Check if package.json version is newer than the latest release
if [ -n "$latest_release_version" ] && [ "$cur_pkg_version" = "$latest_release_version" ]; then
echo "Package version $cur_pkg_version matches latest release $latest_release. Nothing to release."
# Still check if we need to create a new PR for unreleased changes
# Get the latest released version tag
latest_tag="$(git tag --list | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$' | sort -V | tail -1)"
# Check if there are commits since the last release
if [ -n "$latest_tag" ]; then
commits_since_release="$(git rev-list "$latest_tag"..HEAD --count)"
if [ "$commits_since_release" -eq 0 ]; then
echo "No commits since last release $latest_tag"
exit 0
fi
echo "Found $commits_since_release commits since $latest_tag"
fi
# Get the next version and changelog from git-cliff
version="$(git cliff --bumped-version)"
changelog="$(git cliff --bump --unreleased | tail -n +2)"
if [ "${DRY_RUN:-1}" == 1 ]; then
echo "version: $version"
echo "changelog: $changelog"
exit 0
fi
# Check if there are any unreleased changes
if [ -z "$changelog" ] || [ "$changelog" = "<!-- generated by git-cliff -->" ]; then
echo "No unreleased changes found"
exit 0
fi
# Configure git for automated commits
git config user.name mise-en-dev
git config user.email 123107610+mise-en-dev@users.noreply.github.com
# Configure git to use gh's credential helper. The checkout step uses
# persist-credentials: false (per zizmor's artipacked audit), so the
# token isn't written to .git/config and raw `git push` would 403.
gh auth setup-git
# Create a PR with the version bump
npm version "${version#v}" --no-git-tag-version
git add package.json package-lock.json
git status
# Create release branch and commit
git checkout -B release
git commit -m "chore: release $version"
# Push to release branch
git push origin release --force
# Create or update PR
if gh pr create --title "chore: release $version" --body "$changelog" --label "release"; then
echo "Created new release PR"
else
gh pr edit --title "chore: release $version" --body "$changelog"
echo "Updated existing release PR"
fi
elif [ -n "$cur_pkg_version" ] && [ "$cur_pkg_version" != "$latest_release_version" ]; then
# Package version is different from latest release
echo "Package version v$cur_pkg_version is newer than latest release $latest_release."
echo "Release will be created by the release.yml workflow when the PR is merged."
# Exit successfully - the release.yml workflow handles actual release creation
exit 0
else
echo "No action needed"
exit 0
fi

View file

@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
function assert_equal() {
if [ "$1" != "$2" ]; then
echo "Assertion failed: Expected '$1', got '$2'" >&2
return 1
fi
}
EXPECTED_OUTPUT="jq-1.7.1"
assert_equal "$EXPECTED_OUTPUT" "$(mise exec -- jq --version)"
which jq
# windows bash does not seem to work with shims
if [[ "$(uname)" != "MINGW"* ]]; then
assert_equal "$EXPECTED_OUTPUT" "$(jq --version)"
fi
# checking that environment variables set in mise.toml are properly set
assert_equal "${MY_ENV_VAR}" "abc"

View file

@ -1,5 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
git cliff -o CHANGELOG.md --tag "v${npm_package_version:?}"
git add CHANGELOG.md

36
src/cache-save.ts Normal file
View file

@ -0,0 +1,36 @@
import * as cache from '@actions/cache'
import * as core from '@actions/core'
import * as fs from 'fs'
import { rtxDir } from './utils'
export async function run(): Promise<void> {
try {
await cacheRTXTools()
} catch (error) {
if (error instanceof Error) core.setFailed(error.message)
}
}
async function cacheRTXTools(): Promise<void> {
const state = core.getState('CACHE_KEY')
const primaryKey = core.getState('PRIMARY_KEY')
const cachePath = rtxDir()
if (!fs.existsSync(cachePath)) {
throw new Error(`Cache folder path does not exist on disk: ${cachePath}`)
}
if (primaryKey === state) {
core.info(
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
)
return
}
const cacheId = await cache.saveCache([cachePath], primaryKey)
if (cacheId === -1) return
core.info(`Cache saved with the primary key: ${primaryKey}`)
}
run()

View file

@ -1,745 +1,7 @@
import * as cache from '@actions/cache'
import * as io from '@actions/io'
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as glob from '@actions/glob'
import * as crypto from 'crypto'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as Handlebars from 'handlebars'
// Configuration file patterns for cache key generation
const MISE_CONFIG_FILE_PATTERNS = [
`**/.config/mise/config.toml`,
`**/.config/mise/config.lock`,
`**/.config/mise/config.*.toml`,
`**/.config/mise/config.*.lock`,
`**/.config/mise.toml`,
`**/.config/mise.lock`,
`**/.config/mise.*.toml`,
`**/.config/mise.*.lock`,
`**/.mise/config.toml`,
`**/.mise/config.lock`,
`**/.mise/config.*.toml`,
`**/.mise/config.*.lock`,
`**/mise/config.toml`,
`**/mise/config.lock`,
`**/mise/config.*.toml`,
`**/mise/config.*.lock`,
`**/.mise.toml`,
`**/.mise.lock`,
`**/.mise.*.toml`,
`**/.mise.*.lock`,
`**/mise.toml`,
`**/mise.lock`,
`**/mise.*.toml`,
`**/mise.*.lock`,
`**/.tool-versions`
]
// Default cache key template
const DEFAULT_CACHE_KEY_TEMPLATE =
'{{cache_key_prefix}}-{{platform}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}-{{#if file_hash}}{{file_hash}}{{else}}no-config{{/if}}'
const ROOT_MISE_LOCK_FILE_PATTERNS = [/^\.?mise(?:\.[^.]+)?\.lock$/]
const CONFIG_DIR_MISE_LOCK_FILE_PATTERNS = [/^mise(?:\.[^.]+)?\.lock$/]
const CONFIG_MISE_LOCK_FILE_PATTERNS = [/^config(?:\.[^.]+)?\.lock$/]
async function run(): Promise<void> {
try {
await setToolVersions()
await setMiseToml()
let cacheKey: string | undefined
if (core.getBooleanInput('cache')) {
cacheKey = await restoreMiseCache()
} else {
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)
await setEnvVars()
if (core.getBooleanInput('reshim')) {
await miseReshim()
}
await testMise()
if (core.getBooleanInput('install')) {
await miseInstall()
if (cacheKey && core.getBooleanInput('cache_save')) {
await saveCache(cacheKey)
}
}
await miseLs()
const loadEnv = core.getBooleanInput('env')
if (loadEnv) {
await exportMiseEnv()
}
} catch (err) {
if (err instanceof Error) core.setFailed(err.message)
else throw err
}
}
/** /**
* Opt in to mise-wings caching for this workflow run. When * The entrypoint for the action.
* `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 { import { run } from './main'
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')
const cwd = getCwd()
// Check if mise supports --redacted flags based on version input
const supportsRedacted = checkMiseSupportsRedacted()
if (supportsRedacted) {
try {
// First, get the redacted values to identify what needs masking
const redactedOutput = await exec.getExecOutput(
'mise',
['env', '--redacted', '--json'],
{ silent: true, cwd }
)
const redactedVars = JSON.parse(redactedOutput.stdout)
// Mask sensitive values in GitHub Actions
for (const [key, actualValue] of Object.entries(redactedVars)) {
core.setSecret(actualValue as string)
core.info(`Masked sensitive value for: ${key}`)
}
// Then get the actual values
const actualOutput = await exec.getExecOutput('mise', ['env', '--json'], {
cwd
})
const actualVars = JSON.parse(actualOutput.stdout)
// Export all environment variables
for (const [key, value] of Object.entries(actualVars)) {
if (typeof value === 'string') {
core.exportVariable(key, value)
}
}
} catch {
// Fall back to dotenv format if the redacted command fails
core.info('Falling back to dotenv format')
const output = await exec.getExecOutput('mise', ['env', '--dotenv'], {
cwd
})
fs.appendFileSync(process.env.GITHUB_ENV!, output.stdout)
}
} else {
// Fall back to the old --dotenv format for older versions
const output = await exec.getExecOutput('mise', ['env', '--dotenv'], {
cwd
})
fs.appendFileSync(process.env.GITHUB_ENV!, output.stdout)
}
core.endGroup()
}
function cleanVersion(version: string) {
// remove 'v' prefix if present
return version.replace(/^v/, '')
}
function checkMiseSupportsRedacted(): boolean {
const version = core.getInput('version')
// If no version is specified, assume latest which supports redacted
if (!version) {
return true
}
const versionMatch = cleanVersion(version).match(/^(\d+)\.(\d+)\.(\d+)/)
if (!versionMatch) {
// If we can't parse the version, assume it supports redacted
return true
}
const [, year, month, patch] = versionMatch
const yearNum = parseInt(year, 10)
const monthNum = parseInt(month, 10)
const patchNum = parseInt(patch, 10)
// Check if version is >= 2025.8.17
if (yearNum > 2025) return true
if (yearNum === 2025) {
if (monthNum > 8) return true
if (monthNum === 8 && patchNum >= 17) return true
}
return false
}
async function setEnvVars(): Promise<void> {
core.startGroup('Setting env vars')
const set = (k: string, v: string): void => {
if (!process.env[k]) {
core.info(`Setting ${k}=${v}`)
core.exportVariable(k, v)
}
}
if (core.getBooleanInput('experimental')) set('MISE_EXPERIMENTAL', '1')
const logLevel = core.getInput('log_level')
if (logLevel) set('MISE_LOG_LEVEL', logLevel)
const githubToken = core.getInput('github_token')
if (githubToken) {
// Don't use GITHUB_TOKEN, use MISE_GITHUB_TOKEN instead to avoid downstream issues.
set('MISE_GITHUB_TOKEN', githubToken)
} else {
core.warning(
'No MISE_GITHUB_TOKEN provided. You may hit GitHub API rate limits when installing tools from GitHub.'
)
}
set('MISE_TRUSTED_CONFIG_PATHS', process.cwd())
set('MISE_YES', '1')
if (core.getBooleanInput('add_shims_to_path')) {
const shimsDir = path.join(miseDir(), 'shims')
core.info(`Adding ${shimsDir} to PATH`)
core.addPath(shimsDir)
}
}
async function restoreMiseCache(): Promise<string | undefined> {
core.startGroup('Restoring mise cache')
const cachePath = miseDir()
// Use custom cache key if provided, otherwise use default template
const cacheKeyTemplate =
core.getInput('cache_key') || DEFAULT_CACHE_KEY_TEMPLATE
const primaryKey = await processCacheKeyTemplate(cacheKeyTemplate)
core.saveState('PRIMARY_KEY', primaryKey)
core.saveState('MISE_DIR', cachePath)
const cacheKey = await cache.restoreCache([cachePath], primaryKey)
core.setOutput('cache-hit', Boolean(cacheKey))
if (!cacheKey) {
core.info(`mise cache not found for ${primaryKey}`)
return primaryKey
}
core.info(`mise cache restored from key: ${cacheKey}`)
}
async function setupMise(
version: string,
fetchFromGitHub = false
): Promise<void> {
const miseBinDir = path.join(miseDir(), 'bin')
const miseBinPath = path.join(
miseBinDir,
process.platform === 'win32' ? 'mise.exe' : 'mise'
)
const miseShimPath = path.join(miseBinDir, 'mise-shim.exe')
let installedVersion: string | undefined
if (!fs.existsSync(path.join(miseBinPath))) {
core.startGroup(version ? `Download mise@${version}` : 'Setup mise')
await fs.promises.mkdir(miseBinDir, { recursive: true })
const ext =
process.platform === 'win32'
? '.zip'
: version && version.startsWith('2024')
? ''
: (await zstdInstalled())
? '.tar.zst'
: '.tar.gz'
let resolvedVersion = version || (await latestMiseVersion())
resolvedVersion = resolvedVersion.replace(/^v/, '')
let url: string
if (!fetchFromGitHub && !version) {
// Only for latest version
url = `https://mise.jdx.dev/mise-latest-${await getTarget()}${ext}`
} else {
url = `https://github.com/jdx/mise/releases/download/v${resolvedVersion}/mise-v${resolvedVersion}-${await getTarget()}${ext}`
}
installedVersion = resolvedVersion
switch (ext) {
case '.zip': {
await withExtractedZip(url, 'mise.zip', async extractDir => {
const extractedMiseBinDir = path.join(extractDir, 'mise', 'bin')
await io.mv(path.join(extractedMiseBinDir, 'mise.exe'), miseBinPath)
await installWindowsMiseShim(extractedMiseBinDir, miseShimPath)
})
break
}
case '.tar.zst':
await exec.exec('sh', [
'-c',
`curl -fsSL ${url} | tar --zstd -xf - -C ${os.tmpdir()} && mv ${os.tmpdir()}/mise/bin/mise ${miseBinPath}`
])
break
case '.tar.gz':
await exec.exec('sh', [
'-c',
`curl -fsSL ${url} | tar -xzf - -C ${os.tmpdir()} && mv ${os.tmpdir()}/mise/bin/mise ${miseBinPath}`
])
break
default:
await exec.exec('sh', ['-c', `curl -fsSL ${url} > ${miseBinPath}`])
await exec.exec('chmod', ['+x', miseBinPath])
break
}
} else {
const requestedVersion = cleanVersion(core.getInput('version'))
if (requestedVersion !== '') {
installedVersion = await getInstalledMiseVersion(miseBinPath)
if (requestedVersion === installedVersion) {
core.info(`mise already installed`)
} else {
core.info(
`mise already installed (${installedVersion}), but different version requested (${requestedVersion})`
)
await exec.exec(miseBinPath, ['self-update', requestedVersion, '-y'])
core.info(`mise updated to version ${requestedVersion}`)
installedVersion = requestedVersion
}
}
}
await ensureWindowsMiseShim(miseBinPath, miseShimPath, installedVersion)
// compare with provided hash
const want = core.getInput('sha256')
if (want) {
const hash = crypto.createHash('sha256')
const fileBuffer = await fs.promises.readFile(miseBinPath)
const got = hash.update(fileBuffer).digest('hex')
if (got !== want) {
throw new Error(
`SHA256 mismatch: expected ${want}, got ${got} for ${miseBinPath}`
)
}
}
core.addPath(miseBinDir)
}
async function withExtractedZip(
url: string,
archiveName: string,
fn: (extractDir: string) => Promise<void>
): Promise<void> {
const tempDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), 'mise-action-')
)
try {
const archivePath = path.join(tempDir, archiveName)
const extractDir = path.join(tempDir, 'extract')
await exec.exec('curl', ['-fsSL', url, '--output', archivePath])
await exec.exec('unzip', [archivePath, '-d', extractDir])
await fn(extractDir)
} finally {
await io.rmRF(tempDir)
}
}
async function installWindowsMiseShim(
extractedMiseBinDir: string,
miseShimPath: string
): Promise<void> {
if (process.platform !== 'win32') return
const extractedMiseShimPath = path.join(extractedMiseBinDir, 'mise-shim.exe')
if (!fs.existsSync(extractedMiseShimPath)) {
core.info('mise-shim.exe not found in the mise archive; skipping')
return
}
await io.mv(extractedMiseShimPath, miseShimPath)
}
async function ensureWindowsMiseShim(
miseBinPath: string,
miseShimPath: string,
version?: string
): Promise<void> {
if (process.platform !== 'win32') return
if (fs.existsSync(miseShimPath)) return
core.info(
'mise-shim.exe not found next to mise.exe; installing it from the matching release archive'
)
try {
const installedVersion =
version || (await getInstalledMiseVersion(miseBinPath))
const archiveName = `mise-v${installedVersion}-${await getTarget()}.zip`
const url = `https://github.com/jdx/mise/releases/download/v${installedVersion}/${archiveName}`
await withExtractedZip(url, archiveName, async extractDir => {
await installWindowsMiseShim(
path.join(extractDir, 'mise', 'bin'),
miseShimPath
)
})
} catch (err) {
core.warning(
`Failed to install mise-shim.exe: ${errorMessage(err)}. Continuing because mise can fall back to file shim mode on Windows.`
)
}
}
async function getInstalledMiseVersion(miseBinPath: string): Promise<string> {
const versionOutput = await exec.getExecOutput(
miseBinPath,
['version', '--json'],
{ silent: true }
)
const versionJson = JSON.parse(versionOutput.stdout) as { version: string }
return cleanVersion(versionJson.version.split(' ')[0])
}
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err)
}
async function zstdInstalled(): Promise<boolean> {
try {
await exec.exec('zstd', ['--version'])
return true
} catch {
return false
}
}
async function latestMiseVersion(): Promise<string> {
const rsp = await exec.getExecOutput('curl', [
'-fsSL',
'https://mise.jdx.dev/VERSION'
])
return rsp.stdout.trim()
}
async function setToolVersions(): Promise<void> {
const toolVersions = core.getInput('tool_versions')
if (toolVersions) {
await writeFile('.tool-versions', toolVersions)
}
}
async function setMiseToml(): Promise<void> {
const toml = core.getInput('mise_toml')
if (toml) {
await writeFile('mise.toml', toml)
}
}
const testMise = async (): Promise<number> => mise(['--version'])
let supportsLockedInstall: boolean | undefined
const miseInstall = async (): Promise<number> => {
const installArgs = core.getInput('install_args').trim()
const useLocked =
(await shouldUseLockedInstall()) &&
!/(^|\s)--locked(?:\s|$)/.test(installArgs)
const command = [
'install',
...(useLocked ? ['--locked'] : []),
...(installArgs ? [installArgs] : [])
].join(' ')
if (useLocked) {
core.info('Detected a mise lock file, running `mise install --locked`')
}
return mise([command])
}
const miseLs = async (): Promise<number> => mise([`ls`])
const miseReshim = async (): Promise<number> => mise([`reshim`, `-f`])
const mise = async (args: string[]): Promise<number> =>
await core.group(`Running mise ${args.join(' ')}`, async () => {
const cwd = getCwd()
const baseEnv = Object.fromEntries(
Object.entries(process.env).filter(
(entry): entry is [string, string] => entry[1] !== undefined
)
)
const env = core.isDebug()
? { ...baseEnv, MISE_LOG_LEVEL: 'debug' }
: baseEnv
if (args.length === 1) {
return exec.exec(`mise ${args}`, [], {
cwd,
env
})
} else {
return exec.exec('mise', args, { cwd, env })
}
})
const writeFile = async (p: fs.PathLike, body: string): Promise<void> =>
await core.group(`Writing ${p}`, async () => {
core.info(`Body:\n${body}`)
await fs.promises.writeFile(p, body, { encoding: 'utf8' })
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
run() run()
function getCwd(): string {
return (
core.getInput('working_directory') ||
core.getInput('install_dir') ||
process.cwd()
)
}
async function shouldUseLockedInstall(): Promise<boolean> {
if (core.getInput('tool_versions') || core.getInput('mise_toml')) return false
if (!(await miseSupportsLockedInstall())) return false
return hasMiseLockFile(getCwd())
}
async function miseSupportsLockedInstall(): Promise<boolean> {
if (supportsLockedInstall !== undefined) return supportsLockedInstall
const { stdout, stderr } = await exec.getExecOutput(
'mise',
['install', '--help'],
{
cwd: getCwd(),
ignoreReturnCode: true,
silent: true
}
)
supportsLockedInstall = /(^|\s)--locked(?:[\s,]|$)/m.test(
`${stdout}\n${stderr}`
)
return supportsLockedInstall
}
function hasMiseLockFile(startDir: string): boolean {
let dir = path.resolve(startDir)
while (true) {
if (directoryHasMiseLockFile(dir)) return true
const parent = path.dirname(dir)
if (parent === dir) return false
dir = parent
}
}
function directoryHasMiseLockFile(dir: string): boolean {
return (
hasMatchingLockFile(dir, ROOT_MISE_LOCK_FILE_PATTERNS) ||
hasMatchingLockFile(
path.join(dir, '.config'),
CONFIG_DIR_MISE_LOCK_FILE_PATTERNS
) ||
hasMatchingLockFile(path.join(dir, '.config', 'mise'), [
...ROOT_MISE_LOCK_FILE_PATTERNS,
...CONFIG_MISE_LOCK_FILE_PATTERNS
]) ||
hasMatchingLockFile(path.join(dir, '.mise'), [
...ROOT_MISE_LOCK_FILE_PATTERNS,
...CONFIG_MISE_LOCK_FILE_PATTERNS
]) ||
hasMatchingLockFile(path.join(dir, 'mise'), [
...ROOT_MISE_LOCK_FILE_PATTERNS,
...CONFIG_MISE_LOCK_FILE_PATTERNS
])
)
}
function hasMatchingLockFile(dir: string, patterns: RegExp[]): boolean {
try {
const stat = fs.statSync(dir, { throwIfNoEntry: false })
if (!stat?.isDirectory()) return false
return fs
.readdirSync(dir, { withFileTypes: true })
.some(
entry =>
entry.isFile() && patterns.some(pattern => pattern.test(entry.name))
)
} catch {
return false
}
}
function miseDir(): string {
const dir = core.getState('MISE_DIR')
if (dir) return dir
const miseDir = core.getInput('mise_dir')
if (miseDir) return miseDir
const { MISE_DATA_DIR, XDG_DATA_HOME, LOCALAPPDATA } = process.env
if (MISE_DATA_DIR) return MISE_DATA_DIR
if (XDG_DATA_HOME) return path.join(XDG_DATA_HOME, 'mise')
if (process.platform === 'win32' && LOCALAPPDATA)
return path.join(LOCALAPPDATA, 'mise')
return path.join(os.homedir(), '.local', 'share', 'mise')
}
async function saveCache(cacheKey: string): Promise<void> {
await core.group(`Saving mise cache`, async () => {
const cachePath = miseDir()
if (!fs.existsSync(cachePath)) {
throw new Error(`Cache folder path does not exist on disk: ${cachePath}`)
}
const cacheId = await cache.saveCache([cachePath], cacheKey)
if (cacheId === -1) return
core.info(`Cache saved from ${cachePath} with key: ${cacheKey}`)
})
}
async function getTarget(): Promise<string> {
const arch = process.arch === 'arm' ? 'armv7' : process.arch
switch (process.platform) {
case 'darwin':
return `macos-${arch}`
case 'win32':
return `windows-${arch}`
case 'linux':
return `linux-${arch}${(await isMusl()) ? '-musl' : ''}`
default:
throw new Error(`Unsupported platform ${process.platform}`)
}
}
/**
* Identifies the runner image so cached binaries from one provider
* (github-hosted, namespace.so, BuildJet, self-hosted) aren't restored
* onto another provider's image where their compiled-in paths and libc
* versions don't match. GitHub-hosted images export `ImageOS`
* (e.g. "macos15", "ubuntu24"); other runners leave it unset and pool
* under "self-hosted".
*/
function getRunnerImageId(): string {
return process.env.ImageOS || 'self-hosted'
}
async function processCacheKeyTemplate(template: string): Promise<string> {
// Get all available variables
const version = core.getInput('version')
const installArgs = core.getInput('install_args')
const cacheKeyPrefix = core.getInput('cache_key_prefix') || 'mise-v1'
const miseEnv = process.env.MISE_ENV?.replace(/,/g, '-')
const platform = `${await getTarget()}-${getRunnerImageId()}`
// Calculate file hash
const fileHash = await glob.hashFiles(MISE_CONFIG_FILE_PATTERNS.join('\n'))
// Calculate install args hash
let installArgsHash = ''
if (installArgs) {
const tools = installArgs
.split(' ')
.filter(arg => !arg.startsWith('-'))
.sort()
.join(' ')
if (tools) {
installArgsHash = crypto.createHash('sha256').update(tools).digest('hex')
}
}
// Prepare base template data
const baseTemplateData = {
version,
cache_key_prefix: cacheKeyPrefix,
platform,
file_hash: fileHash,
mise_env: miseEnv,
install_args_hash: installArgsHash
}
// Calculate the default cache key by processing the default template
const defaultTemplate = Handlebars.compile(DEFAULT_CACHE_KEY_TEMPLATE)
const defaultCacheKey = defaultTemplate(baseTemplateData)
// Prepare final template data including the default cache key and env variables
const templateData = {
...baseTemplateData,
default: defaultCacheKey,
env: process.env
}
// Compile and execute the user's template
const compiledTemplate = Handlebars.compile(template)
return compiledTemplate(templateData)
}
async function isMusl() {
// `ldd --version` always returns 1 and print to stderr
const { stderr } = await exec.getExecOutput('ldd', ['--version'], {
failOnStdErr: false,
ignoreReturnCode: true
})
return stderr.indexOf('musl') > -1
}

105
src/main.ts Normal file
View file

@ -0,0 +1,105 @@
import * as cache from '@actions/cache'
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as glob from '@actions/glob'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import { rtxDir } from './utils'
async function run(): Promise<void> {
await setToolVersions()
await restoreRTXCache()
await setupRTX()
await setEnvVars()
await exec.exec('rtx', ['--version'])
const install = core.getBooleanInput('install', { required: false })
if (install) {
await exec.exec('rtx', ['install'])
}
await setPaths()
}
async function setEnvVars(): Promise<void> {
if (!process.env['RTX_TRUSTED_CONFIG_PATHS']) {
core.exportVariable(
'RTX_TRUSTED_CONFIG_PATHS',
path.join(process.cwd(), '.rtx.toml')
)
}
if (!process.env['RTX_YES']) {
core.exportVariable('RTX_YES', 'yes')
}
}
async function restoreRTXCache(): Promise<void> {
const cachePath = rtxDir()
const fileHash = await glob.hashFiles(`**/.tool-versions\n**/.rtx.toml`)
const primaryKey = `rtx-tools-${getOS()}-${os.arch()}-${fileHash}`
core.saveState('PRIMARY_KEY', primaryKey)
const cacheKey = await cache.restoreCache([cachePath], primaryKey)
core.setOutput('cache-hit', Boolean(cacheKey))
if (!cacheKey) {
core.info(`rtx cache not found for ${getOS()}-${os.arch()} tool versions`)
return
}
core.saveState('CACHE_KEY', cacheKey)
core.info(`rtx cache restored from key: ${cacheKey}`)
}
async function setupRTX(): Promise<void> {
const rtxBinDir = path.join(rtxDir(), 'bin')
const url = `https://rtx.pub/rtx-latest-${getOS()}-${os.arch()}`
await fs.promises.mkdir(rtxBinDir, { recursive: true })
await exec.exec('curl', [url, '--output', path.join(rtxBinDir, 'rtx')])
await exec.exec('chmod', ['+x', path.join(rtxBinDir, 'rtx')])
core.addPath(rtxBinDir)
}
// returns true if tool_versions was set
async function setToolVersions(): Promise<boolean> {
const toolVersions = core.getInput('tool_versions', { required: false })
if (toolVersions) {
await fs.promises.writeFile('.tool-versions', toolVersions, {
encoding: 'utf8'
})
return true
}
return false
}
function getOS(): string {
switch (process.platform) {
case 'darwin':
return 'macos'
default:
return process.platform
}
}
async function setPaths(): Promise<void> {
for (const binPath of await getBinPaths()) {
core.addPath(binPath)
}
}
async function getBinPaths(): Promise<string[]> {
const output = await exec.getExecOutput('rtx', ['bin-paths'])
return output.stdout.split('\n')
}
if (require.main === module) {
try {
run()
} catch (err) {
if (err instanceof Error) {
core.setFailed(err.message)
} else throw err
}
}
export { run }

12
src/utils.ts Normal file
View file

@ -0,0 +1,12 @@
import * as os from 'os'
import * as path from 'path'
export function rtxDir(): string {
if (process.env.RTX_DATA_HOME) {
return process.env.RTX_DATA_HOME
}
if (process.env.XDG_DATA_HOME) {
return path.join(process.env.XDG_DATA_HOME, 'rtx')
}
return path.join(os.homedir(), '.local/share/rtx')
}

View file

@ -13,9 +13,7 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"newLine": "lf", "newLine": "lf"
"isolatedModules": true,
"allowSyntheticDefaultImports": true
}, },
"exclude": ["./dist", "./node_modules", "./__tests__", "./coverage"] "exclude": ["./dist", "./node_modules", "./__tests__", "./coverage"]
} }