Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/actions/ai-step/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,12 @@ make test-ai-step
```

Runs the bats suite in `test/` against `src/resolve-config.sh`.

### Refresh a GitHub MCP credential without a session

Use `loft-sh/github-actions/.github/actions/ai-step/refresh@ai-step/v1` with
`vault-id`, `github-app-id`, `github-app-installation-id`,
`github-app-private-key`, and `github-app-repository`. The caller must supply
Anthropic workload identity federation environment variables and an identity
token. This renews the vault bearer once and starts no agent session. Failure
fails the step without printing API error details.
33 changes: 33 additions & 0 deletions .github/actions/ai-step/refresh/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Refresh GitHub MCP credential
description: Renew a repository-scoped GitHub App token in a managed-agent vault without starting a session.
inputs:
vault-id:
description: Managed-agent vault ID.
required: true
github-app-id:
description: GitHub App ID.
required: true
github-app-installation-id:
description: GitHub App installation ID.
required: true
github-app-private-key:
description: GitHub App private key.
required: true
github-app-repository:
description: Single repository to scope the token to.
required: true
runs:
using: composite
steps:
- name: Install Anthropic SDK
shell: bash
run: python3 -m pip install --quiet --disable-pip-version-check anthropic
- name: Refresh credential
shell: bash
env:
INPUT_VAULT_ID: ${{ inputs.vault-id }}
INPUT_GITHUB_APP_ID: ${{ inputs.github-app-id }}
INPUT_GITHUB_APP_INSTALLATION_ID: ${{ inputs.github-app-installation-id }}
INPUT_GITHUB_APP_PRIVATE_KEY: ${{ inputs.github-app-private-key }}
INPUT_GITHUB_APP_REPOSITORY: ${{ inputs.github-app-repository }}
run: python3 "$GITHUB_ACTION_PATH/../src/refresh_github.py"
30 changes: 30 additions & 0 deletions .github/actions/ai-step/src/refresh_github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: Apache-2.0
"""Refresh the GitHub MCP bearer without creating an agent session."""
import os
import sys


def main():
try:
from anthropic import Anthropic
from github_app import GitHubCredential

client = Anthropic(default_headers={"anthropic-beta": "managed-agents-2026-04-01"})
credential = GitHubCredential(
client, os.environ.get("INPUT_VAULT_ID", ""),
os.environ.get("INPUT_GITHUB_APP_ID", ""),
os.environ.get("INPUT_GITHUB_APP_INSTALLATION_ID", ""),
os.environ.get("INPUT_GITHUB_APP_PRIVATE_KEY", ""),
os.environ.get("INPUT_GITHUB_APP_REPOSITORY", ""),
)
credential.renew(force=True)
except Exception:
# SDK and API errors can contain credentials. Never print their details.
print("::error::GitHub MCP credential refresh failed", file=sys.stderr)
return 1
print("GitHub MCP credential refreshed")
return 0


if __name__ == "__main__":
sys.exit(main())
30 changes: 30 additions & 0 deletions .github/actions/ai-step/test/test_refresh_github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import importlib.util
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock

spec = importlib.util.spec_from_file_location("refresh_github", Path(__file__).parents[1] / "src/refresh_github.py")
refresh = importlib.util.module_from_spec(spec)
spec.loader.exec_module(refresh)


def test_renews_once_without_creating_session(monkeypatch):
client = Mock()
credential = Mock()
factory = Mock(return_value=credential)
monkeypatch.setitem(sys.modules, "anthropic", SimpleNamespace(Anthropic=Mock(return_value=client)))
monkeypatch.setitem(sys.modules, "github_app", SimpleNamespace(GitHubCredential=factory))
for name, value in {"INPUT_VAULT_ID": "vault", "INPUT_GITHUB_APP_ID": "123", "INPUT_GITHUB_APP_INSTALLATION_ID": "456", "INPUT_GITHUB_APP_PRIVATE_KEY": "key", "INPUT_GITHUB_APP_REPOSITORY": "owner/repo"}.items():
monkeypatch.setenv(name, value)
assert refresh.main() == 0
factory.assert_called_once_with(client, "vault", "123", "456", "key", "owner/repo")
credential.renew.assert_called_once_with(force=True)
client.beta.sessions.create.assert_not_called()


def test_failure_does_not_print_exception(monkeypatch, capsys):
monkeypatch.setitem(sys.modules, "anthropic", SimpleNamespace(Anthropic=Mock(side_effect=ValueError("secret"))))
monkeypatch.setitem(sys.modules, "github_app", SimpleNamespace(GitHubCredential=Mock()))
assert refresh.main() == 1
assert "secret" not in capsys.readouterr().err
Loading