# github-app-forge
> **⚠ STATUS: shelved (2026-04-27).**
>
> The GitHub App Manifest flow this tool wraps has too much schema-validator
> friction (rejects `null` for optional Option fields, then rejects
> "url wasn't supplied" when present-but-form-encoded, with no clean signal
> from GitHub about which) for the savings to be worth it. The canonical path
> for first-pass GitHub App registration is now manual UI (~5 min, once a year
> per app) — see the runbook in any pleme-io cluster's
> `clusters/<cluster>/infrastructure/arc/RUNBOOK.md`.
>
> This repo is preserved for two reasons:
> 1. The Rust scaffolding (manifest serde types + JWT signer + CredentialSink
> trait + composite-action structure) is reusable if/when GitHub improves
> the Manifest API or a similar flow lands elsewhere.
> 2. If rotation cadence ever justifies revisiting (e.g. monthly App key
> rotation across N clusters), the `rotate` command is already authored.
>
> Do not depend on this tool for new pleme-io workflows. Use a pre-existing
> org PAT (e.g. an `_CI_GITHUB_TOKEN` org secret) for ARC runner registration
> until/unless the situation changes.
Declarative GitHub App lifecycle management for pleme-io. Replaces ~10 minutes of UI form-filling per app with a typed YAML manifest and one browser confirmation.
## Why
GitHub doesn't expose a pure REST API for creating GitHub Apps (anti-supply-chain — one app can't silently provision another). The only programmatic path is the **Manifest flow**, which still requires exactly one browser click on a confirmation page. Everything before and after that click — manifest authoring, credential capture, sink writing, repo installation, rotation — IS automatable.
`github-app-forge` collapses the manual steps to:
1. Author a typed YAML manifest (one-time per app)
2. Run `github-app-forge create manifests/<app>.yaml`
3. Click "Create from manifest" in the browser tab that opens
4. Click "Install" in the second tab that opens (with target repos pre-selected)
5. Tool captures App ID, private key, installation ID + writes them to the configured sink (SOPS, file, future: Akeyless)
Everything else — repo additions later, credential rotation, value-stub generation for HelmReleases — is fully API-driven via the App's own JWT.
## Install
```bash
nix run github:pleme-io/github-app-forge -- create manifests/<app>.yaml
```
Or via home-manager:
```nix
imports = [ inputs.github-app-forge.homeManagerModules.default ];
programs.github-app-forge = {
enable = true;
package = inputs.github-app-forge.packages.${pkgs.system}.default;
};
```
## Usage
```bash
# Validate a manifest without network calls
github-app-forge validate manifests/rio.yaml
# Create the app + install + write credentials
github-app-forge create manifests/rio.yaml
# Add a repo to an existing installation (fully API-driven)
github-app-forge install --app pleme-arc-rio --owner pleme-io \
--repos new-repo \
--credentials ~/.local/share/github-app-forge/pleme-arc-rio.yaml
# Emit a HelmRelease values stub
github-app-forge values \
--credentials ~/.local/share/github-app-forge/pleme-arc-rio.yaml
```
## Sinks
| `stdout` (default) | Dry-run / debugging |
| `file` | Plaintext YAML — testing only |
| `sops` | Canonical sink for pleme-io GitOps clusters: renders K8s Secret YAML matching `pleme-arc-controller`'s expected shape, runs `sops --encrypt --in-place` |
| `akeyless` | Stub — future Akeyless integration via `akeyless-nix` |
## Manifest schema
See `src/manifest.rs` for the typed Rust source of truth. The YAML mirrors GitHub's [Manifest API schema](https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest) for the fields under `github:`, plus `github-app-forge`-specific outer keys (`name`, `install_target`, `owner`, `install_repos`, `sink`).
## Security
- Private keys never leave the local machine without going through the configured sink
- SOPS sink runs `sops --encrypt --in-place` so the unencrypted PEM is written to disk only momentarily before encryption (atomic rename via sops)
- App JWT generation happens locally; tokens are never persisted
- Installation tokens issued for repo-add operations are short-lived (15min) and not stored
## License
MIT