Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
86b2f3c
Wire the action from OIDC to a holder-bound warrant and replay the de…
aimable100 Sep 3, 2026
35c71dd
Add a live GitHub e2e that comments through the holder and closes the…
aimable100 Sep 3, 2026
ab64b43
Close the holder at warrant expiry and pin the action's Python deps b…
aimable100 Sep 3, 2026
658c1f7
Speak Cloud's identifier-free exchange and present the full warrant s…
aimable100 Sep 3, 2026
2b29bc2
Align the GitHub Action with Cloud's compact exchange contract.
aimable100 Sep 3, 2026
2b5e9b5
Pin Linux extras in the action lockfile so a clean runner can install.
aimable100 Sep 3, 2026
c3d3f59
Package the Ubuntu Tenuo wheel into the action artifact.
aimable100 Sep 3, 2026
8c7cd7e
Require a core-derived terminal leaf before the gateway allows a call.
aimable100 Sep 3, 2026
5e9b101
Mint GitHub App tokens from a Secret-mounted PEM so the gateway can c…
aimable100 Sep 4, 2026
762b9b9
Fail /ready unless the Secret-mounted keys can sign.
aimable100 Sep 4, 2026
d215642
Add init-secrets so a partner can create the Secret mount without pas…
aimable100 Sep 4, 2026
ddacdc5
Build the concierge image from the published Tenuo wheel.
aimable100 Sep 4, 2026
17c26cb
Let doctor prove the customer gateway before Cloud exchange exists.
aimable100 Sep 4, 2026
acde973
Add box so a partner can prove Secret keys sign before compose.
aimable100 Sep 4, 2026
34e6f3e
Make compose healthy only after /ready can sign, and start against Fa…
aimable100 Sep 4, 2026
9b20039
Lock the Cloud customer-gateway contract and add the split Helm chart.
aimable100 Sep 5, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,10 @@ jobs:
- name: GitHub Actions gateway tests
working-directory: github-actions
run: |
python package_runtime.py
test -n "$(ls vendor/tenuo-*.whl)"
uv pip install --system pytest pyyaml PyJWT cryptography starlette httpx
pytest -v
pytest -v -m "not live"

# ============================================================================
# TEMPORAL INTEGRATION (live Temporal server)
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: GitHub Actions package

on:
pull_request:
push:
paths:
- github-actions/**
- tenuo-python/**
- tenuo-core/**
- .github/workflows/github-actions.yml
workflow_dispatch:

permissions:
contents: read

jobs:
test:
name: github-actions
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install Rust
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable

- name: Cache Rust
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: tenuo-python

- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

- name: Build tenuo and run package tests
run: |
pip install uv
uv pip install --system maturin
(cd tenuo-python && maturin build --release)
python github-actions/package_runtime.py
test -n "$(ls github-actions/vendor/tenuo-*.whl)"
uv pip install --system tenuo-python/target/wheels/*.whl
uv pip install --system pytest pyyaml PyJWT cryptography starlette httpx
(cd github-actions && pytest -q -m "not live")
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ reproduce_pickle.py
.DS_Store
Thumbs.db

# Packaged Tenuo wheels for the GitHub Action (built, not committed)
github-actions/vendor/*.whl

# Python
__pycache__/
*.pyc
Expand Down
3 changes: 3 additions & 0 deletions charts/tenuo-github-actions/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git
.gitignore
ci/
16 changes: 16 additions & 0 deletions charts/tenuo-github-actions/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v2
name: tenuo-github-actions
description: Tenuo for GitHub Actions — split exchange and gateway beside the runners
type: application
version: 0.1.0
appVersion: "0.1.0"
keywords:
- tenuo
- github-actions
- authorization
home: https://github.com/tenuo-ai/tenuo
sources:
- https://github.com/tenuo-ai/tenuo
maintainers:
- name: Tenuo Team
email: team@tenuo.ai
54 changes: 54 additions & 0 deletions charts/tenuo-github-actions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Tenuo for GitHub Actions

Two Deployments, two ServiceAccounts, `secret` profile. There is no values knob that merges the identities.

`signing.profile=kms` is not implemented in this image. The chart fails closed if you select it.

## Install

Create two Secrets. The exchange Secret holds only the issuer key. The gateway Secret holds the receipt key and the GitHub App PEM.

```bash
kubectl create secret generic tenuo-exchange-keys \
--from-file=issuer.pem=/path/to/issuer.pem

kubectl create secret generic tenuo-gateway-keys \
--from-file=receipt.pem=/path/to/receipt.pem \
--from-file=app.pem=/path/to/app.pem

helm install tenuo ./charts/tenuo-github-actions \
--set trust.rootPublicKeys[0]="$(xxd -p -c 256 /path/to/issuer.pub)" \
--set exchange.audience=tenuo:org/acme \
--set exchange.secrets.existingSecret=tenuo-exchange-keys \
--set gateway.secrets.existingSecret=tenuo-gateway-keys \
--set github.appId=123456
```

Prefer pinning `image.digest` once the image is published. Until then, build the concierge image from `github-actions/Dockerfile` and set `image.registry`, `image.repository`, and `image.tag`.

## Required values

| Value | Meaning |
|---|---|
| `trust.rootPublicKeys` | Hex public keys the gateway verifies against |
| `exchange.audience` | OIDC audience the job must request |
| `exchange.secrets.existingSecret` | Secret with `issuer.pem` |
| `gateway.secrets.existingSecret` | Secret with `receipt.pem` and `app.pem` |
| `github.appId` | GitHub App id |

The two `existingSecret` names must differ. A shared Secret is a chart error.

## After install

Point the action at the two Services. Do not point `TENUO_GATEWAY_URL` at Tenuo Cloud.
Cloud customers set `TENUO_EXCHANGE_URL` to the Cloud API and keep the gateway Service.

```text
TENUO_GATEWAY_URL=http://<release>-tenuo-github-actions-gateway.<ns>.svc:8000
TENUO_EXCHANGE_URL=http://<release>-tenuo-github-actions-exchange.<ns>.svc:8000
TENUO_EXCHANGE_AUDIENCE=tenuo:org/acme
TENUO_TRUSTED_ROOTS=<hex of trust.rootPublicKeys>
```

Action pin until v1: `uses: tenuo-ai/tenuo/github-actions@<sha>` with
`gateway_url`, `exchange_url`, `audience`, and `trusted_roots`.
12 changes: 12 additions & 0 deletions charts/tenuo-github-actions/ci/secret-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
trust:
rootPublicKeys:
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
exchange:
audience: tenuo:org/acme
secrets:
existingSecret: tenuo-exchange-keys
gateway:
secrets:
existingSecret: tenuo-gateway-keys
github:
appId: "123456"
20 changes: 20 additions & 0 deletions charts/tenuo-github-actions/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{- include "tenuo-github-actions.validate" . }}
Tenuo for GitHub Actions (secret profile)

Exchange: {{ include "tenuo-github-actions.exchangeName" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.exchange.service.port }}
Gateway: {{ include "tenuo-github-actions.gatewayName" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.gateway.service.port }}

Set these org/repo variables, then pin the action SHA:

TENUO_EXCHANGE_URL=http://{{ include "tenuo-github-actions.exchangeName" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.exchange.service.port }}
TENUO_GATEWAY_URL=http://{{ include "tenuo-github-actions.gatewayName" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.gateway.service.port }}
TENUO_EXCHANGE_AUDIENCE={{ .Values.exchange.audience }}

The exchange Secret must contain {{ .Values.signing.secret.issuerKey }}.
The gateway Secret must contain {{ .Values.signing.secret.receiptKey }} and {{ .Values.signing.secret.githubAppKey }}.
Those Secrets are not interchangeable. Do not mount the issuer key on the gateway.

Ready is a sign self-test, not /health:

kubectl rollout status deploy/{{ include "tenuo-github-actions.exchangeName" . }}
kubectl rollout status deploy/{{ include "tenuo-github-actions.gatewayName" . }}
108 changes: 108 additions & 0 deletions charts/tenuo-github-actions/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{{- define "tenuo-github-actions.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "tenuo-github-actions.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{- define "tenuo-github-actions.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "tenuo-github-actions.labels" -}}
helm.sh/chart: {{ include "tenuo-github-actions.chart" . }}
{{ include "tenuo-github-actions.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "tenuo-github-actions.selectorLabels" -}}
app.kubernetes.io/name: {{ include "tenuo-github-actions.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{- define "tenuo-github-actions.exchangeSelectorLabels" -}}
{{ include "tenuo-github-actions.selectorLabels" . }}
app.kubernetes.io/component: exchange
{{- end }}

{{- define "tenuo-github-actions.gatewaySelectorLabels" -}}
{{ include "tenuo-github-actions.selectorLabels" . }}
app.kubernetes.io/component: gateway
{{- end }}

{{- define "tenuo-github-actions.image" -}}
{{- $registry := .Values.image.registry -}}
{{- $repo := .Values.image.repository -}}
{{- $ref := "" -}}
{{- if .Values.image.digest }}
{{- $ref = printf "@%s" .Values.image.digest -}}
{{- else }}
{{- $ref = printf ":%s" (.Values.image.tag | default .Chart.AppVersion) -}}
{{- end }}
{{- if $registry }}
{{- printf "%s/%s%s" $registry $repo $ref }}
{{- else }}
{{- printf "%s%s" $repo $ref }}
{{- end }}
{{- end }}

{{- define "tenuo-github-actions.exchangeName" -}}
{{- printf "%s-exchange" (include "tenuo-github-actions.fullname" .) | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "tenuo-github-actions.gatewayName" -}}
{{- printf "%s-gateway" (include "tenuo-github-actions.fullname" .) | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "tenuo-github-actions.exchangeServiceAccountName" -}}
{{- if .Values.exchange.serviceAccount.create }}
{{- default (include "tenuo-github-actions.exchangeName" .) .Values.exchange.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.exchange.serviceAccount.name }}
{{- end }}
{{- end }}

{{- define "tenuo-github-actions.gatewayServiceAccountName" -}}
{{- if .Values.gateway.serviceAccount.create }}
{{- default (include "tenuo-github-actions.gatewayName" .) .Values.gateway.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.gateway.serviceAccount.name }}
{{- end }}
{{- end }}

{{- define "tenuo-github-actions.validate" -}}
{{- if ne .Values.signing.profile "secret" }}
{{- fail "signing.profile must be secret. kms is not implemented in this image yet." }}
{{- end }}
{{- if not .Values.trust.rootPublicKeys }}
{{- fail "trust.rootPublicKeys is required" }}
{{- end }}
{{- if not .Values.exchange.audience }}
{{- fail "exchange.audience is required" }}
{{- end }}
{{- if not .Values.exchange.secrets.existingSecret }}
{{- fail "exchange.secrets.existingSecret is required (issuer.pem)" }}
{{- end }}
{{- if not .Values.gateway.secrets.existingSecret }}
{{- fail "gateway.secrets.existingSecret is required (receipt.pem and app.pem)" }}
{{- end }}
{{- if eq .Values.exchange.secrets.existingSecret .Values.gateway.secrets.existingSecret }}
{{- fail "exchange and gateway cannot share a key Secret" }}
{{- end }}
{{- if not .Values.github.appId }}
{{- fail "github.appId is required" }}
{{- end }}
{{- end }}
56 changes: 56 additions & 0 deletions charts/tenuo-github-actions/templates/exchange-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{{- include "tenuo-github-actions.validate" . }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "tenuo-github-actions.exchangeName" . }}
labels:
{{- include "tenuo-github-actions.labels" . | nindent 4 }}
app.kubernetes.io/component: exchange
data:
gateway.yaml: |
version: 1
trust:
root_public_keys:
{{- range .Values.trust.rootPublicKeys }}
- {{ . | quote }}
{{- end }}
require_pop: {{ .Values.trust.requirePop }}
clock_tolerance_seconds: {{ .Values.trust.clockToleranceSeconds }}
signing:
profile: secret
secret:
mount: {{ .Values.signing.secret.mount | quote }}
issuer_key: {{ .Values.signing.secret.issuerKey | quote }}
exchange:
audience: {{ .Values.exchange.audience | quote }}
issuer: {{ .Values.exchange.issuer | quote }}
ttl_max: {{ .Values.exchange.ttlMax | quote }}
{{- if .Values.exchange.jwksUrl }}
jwks_url: {{ .Values.exchange.jwksUrl | quote }}
{{- end }}
conditions:
{{- if .Values.exchange.conditions.repositoryOwnerId }}
repository_owner_id: {{ .Values.exchange.conditions.repositoryOwnerId | quote }}
{{- end }}
{{- if .Values.exchange.conditions.repositoryId }}
repository_id:
{{- range .Values.exchange.conditions.repositoryId }}
- {{ . | quote }}
{{- end }}
{{- end }}
{{- if .Values.exchange.conditions.jobWorkflowRef }}
job_workflow_ref: {{ .Values.exchange.conditions.jobWorkflowRef | quote }}
{{- end }}
{{- if .Values.exchange.conditions.eventName }}
event_name:
{{- range .Values.exchange.conditions.eventName }}
- {{ . | quote }}
{{- end }}
{{- end }}
ceiling:
repositories:
{{- range .Values.ceiling.repositories }}
- {{ . | quote }}
{{- end }}
receipts:
path: {{ .Values.receipts.path | quote }}
Loading
Loading