Skip to content

Console App Smoke #1331

Console App Smoke

Console App Smoke #1331

name: Console App Smoke
# Verifies that the kubestellar-console-bot GitHub App credentials are
# valid and usable by minting a JWT and exchanging it for a short-lived
# installation token. Does NOT create any issues — purely a credential
# health check.
#
# Also runs the rewards classifier unit tests so any change to the
# attribution logic is caught before it hits the leaderboard.
#
# Runs every 30 minutes. Opens a priority/critical issue on failure
# (deduplicated by label). Catches:
# - App private key rotation not propagated to the repo secret
# - Installation revoked on the target account
# - Permissions removed (e.g. Issues:write)
# - GitHub API format changes
on:
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
permissions: read-all # default read; writes scoped to job below
concurrency:
group: console-app-smoke
cancel-in-progress: true
jobs:
# ── 1. GitHub App credential health ──────────────────────────────
credential-smoke:
permissions:
issues: write
if: github.repository == 'kubestellar/console'
name: App Credential Smoke
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install pyjwt (for signing App JWT)
run: pip install --quiet pyjwt==2.12.1 cryptography==46.0.7
- name: Mint App JWT and exchange for installation token
env:
APP_ID: ${{ secrets.KUBESTELLAR_CONSOLE_APP_ID }}
INSTALLATION_ID: ${{ secrets.KUBESTELLAR_CONSOLE_APP_INSTALLATION_ID }}
PRIVATE_KEY: ${{ secrets.KUBESTELLAR_CONSOLE_APP_PRIVATE_KEY }}
run: |
if [ -z "$APP_ID" ] || [ -z "$INSTALLATION_ID" ] || [ -z "$PRIVATE_KEY" ]; then
echo "::error::One or more App secrets missing"
exit 1
fi
# Sign a short-lived App JWT with the private key.
JWT=$(python3 <<'PY'
import jwt, os, time
key = os.environ["PRIVATE_KEY"]
now = int(time.time())
token = jwt.encode(
{"iat": now - 60, "exp": now + 540, "iss": os.environ["APP_ID"]},
key, algorithm="RS256",
)
print(token)
PY
)
# Exchange JWT for an installation access token.
RESP=$(curl -sS -o /tmp/app-token.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer $JWT" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/app/installations/$INSTALLATION_ID/access_tokens")
if [ "$RESP" != "201" ]; then
echo "::error::Installation token request returned HTTP $RESP"
cat /tmp/app-token.json
exit 1
fi
# Verify the token has the expected permissions.
PERMS=$(python3 -c "import json; d=json.load(open('/tmp/app-token.json')); print(','.join(d.get('permissions', {}).keys()))")
echo "Granted permissions: $PERMS"
if ! echo "$PERMS" | grep -q "issues"; then
echo "::error::Installation token lacks 'issues' permission"
exit 1
fi
# Verify the token actually works by calling a trivial endpoint.
TOKEN=$(python3 -c "import json; print(json.load(open('/tmp/app-token.json'))['token'])")
HTTP=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
"https://api.github.com/installation/repositories")
if [ "$HTTP" != "200" ]; then
echo "::error::Token did not authenticate /installation/repositories — got HTTP $HTTP"
exit 1
fi
echo "✓ App credentials healthy"
- name: Create issue on failure
if: failure()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'console-app-smoke-failure',
per_page: 1,
});
if (existing.data.length > 0) {
console.log(`Issue already open: #${existing.data[0].number}`);
return;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `🚨 kubestellar-console-bot App credentials failed health check`,
body: [
'## Console App Credential Failure',
'',
`**Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
`**Time:** ${new Date().toISOString()}`,
'',
'The \`kubestellar-console-bot\` GitHub App credentials could not be exchanged for a valid installation token.',
'',
'### Likely causes',
'- Private key rotated, repo secret not updated',
'- App installation revoked on the target account',
'- Required permissions removed from the App',
'- GitHub API outage',
'',
'### Effect',
'While this is broken, **console-submitted issues will not get App attribution** — the backend falls back to the PAT (\`FEEDBACK_GITHUB_TOKEN\`). Rewards will continue to award points but without the anti-gaming guarantee.',
].join('\n'),
labels: ['console-app-smoke-failure', 'priority/critical'],
});
# ── 2. Rewards classifier unit tests ────────────────────────────
classifier-contract:
permissions:
issues: write
if: github.repository == 'kubestellar/console'
name: Rewards Classifier Contract
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Run classifier + attribution tests
run: |
go test -timeout 60s -v -run \
'TestNewGitHubApp|TestExpectedApp|TestIsConsoleApp|TestRequiresApp|TestClassifyIssue' \
./pkg/api/handlers/...