Create resources in GitHub ⏱️ 20m
The pre-staged credential below only exists on the managed workshop cluster. On a solo local cluster the operator hasn't staged anything for you, so use your own GitHub credentials instead. Create a fine-grained personal access token with these settings:
- Resource owner: your own account, or an org you control
- Repository access: "All repositories" (simplest) or pick the specific repos you'll let Crossplane manage
- Repository permissions:
Contents: Read and write,Administration: Read and write,Metadata: Read-only(auto-selected) - Expiration: whatever you're comfortable with; 7–30 days is plenty for a workshop walkthrough
Administration: Read and write is what lets the provider create and
delete Repository resources; Contents covers everything inside them
(branches, files, branch-protection rules). Without Administration the
Repository MR will reconcile to Synced=False with a 403 from the
GitHub API.
Then create the Secret yourself, replacing <your-pat> with the token
and <owner> with your GitHub username or org:
kubectl -n crossplane-system create secret generic github-app-credentials \
--from-literal=credentials='{"token":"<your-pat>","owner":"<owner>"}'
The rest of the module works unchanged — you just won't be writing into
riccap-demo-org, you'll be writing into your own namespace on
github.com. The pair-<id>-* naming convention doesn't apply.
6.1 Before you start ⏱️ 3m
Every provider you've used so far has acted on the same Kubernetes cluster — provider-kubernetes and provider-helm both terminate at this cluster's API server. provider-github is different: it talks to GitHub's REST/GraphQL API and reconciles real resources (repos, teams, branch protection rules) in a real GitHub organization.
For this module the org is riccap-demo-org — a sandbox set up so workshop participants can create real repositories without needing their own GitHub credentials. A single GitHub App is installed on the org and its credentials are pre-staged inside your workshop cluster, so you don't have to handle a token yourself.
Two ground rules for the shared sandbox:
- Name everything
pair-<your-id>-<thing>. The App can write to any repo in the org, so the only thing keeping pairs from stepping on each other is the prefix convention. Repos that don't start withpair-<your-id>-belong to someone else. - No deletes. The App is intentionally not granted
delete_repo, so a straykubectl delete repository.repo.github.upbound.iowill orphan the GitHub repo (Crossplane removes the MR; the repo lives on). That's by design — leftover repos get cleaned up out-of-band by the operator.
You're about to: install provider-github, point a ProviderConfig at the pre-staged credential, create one Repository MR, and watch it appear on github.com.
6.2 Confirm the credential is in place ⏱️ 2m
The operator has pre-staged the credential in your crossplane-system namespace. Confirm it's there:
kubectl -n crossplane-system get secret github-app-credentials
Expected output:
NAME TYPE DATA AGE
github-app-credentials Opaque 1 5m
The Secret has a single credentials key holding a JSON blob the upstream provider expects: {"app_auth":[{"id":"…","installation_id":"…","pem_file":"…"}],"owner":"riccap-demo-org"}. Don't dump the contents — it's a real private key.
If the Secret isn't there, the operator hasn't staged the credential yet. Flag it in the workshop chat — you can't proceed without it.
6.3 Install the provider ⏱️ 5m
1. Apply the Provider manifest
kubectl apply -f - <<'EOF'
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-github
spec:
package: xpkg.crossplane.io/crossplane-contrib/provider-upjet-github:v0.19.0
EOF
The Provider resource is named provider-github (short, friendly) but the package itself is provider-upjet-github — the crossplane-contrib upjet-based provider that replaced the archived provider-github. Crossplane pulls the image and starts a Pod in crossplane-system. This takes 30–60 seconds.
kubectl get provider.pkg.crossplane.io provider-github
Expected output:
NAME INSTALLED HEALTHY PACKAGE
provider-github True True xpkg.crossplane.io/crossplane-contrib/provider-upjet-github:v0.19.0
2. Wire the ProviderConfig to the staged Secret
kubectl apply -f - <<'EOF'
apiVersion: github.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: github-app-credentials
key: credentials
EOF
The ProviderConfig points at the Secret you just confirmed in 6.2. The provider Pod reads the JSON blob, exchanges the App's PEM + installation ID for an installation token, and uses that token to talk to the GitHub API on behalf of riccap-demo-org.
When the tile turns green, the provider is installed and healthy.
6.4 Create your first repo ⏱️ 7m
1. Apply a Repository MR
Substitute <your-pair-id> with your pair ID (the value the <PairId /> block at the top of this page shows). Names that don't match pair-<your-id>-* will collide with other pairs' work — please stick to the convention.
kubectl apply -f - <<'EOF'
apiVersion: repo.github.upbound.io/v1alpha1
kind: Repository
metadata:
name: pair-<your-pair-id>-hello
spec:
forProvider:
name: pair-<your-pair-id>-hello
description: "Created by Crossplane from the workshop"
visibility: public
autoInit: true
providerConfigRef:
name: default
EOF
spec.forProvider.name is the actual GitHub repo name and is required — the provider does not default it from metadata.name. Keep them the same here so there's only one identifier to track.
2. Watch it reconcile
kubectl get repository.repo.github.upbound.io
Expected output (after ~10s):
NAME READY SYNCED EXTERNAL-NAME AGE
pair-<your-pair-id>-hello True True pair-<your-pair-id>-hello 15s
Then open it in your browser:
https://github.com/riccap-demo-org/pair-<your-pair-id>-hello
The repo exists. You created a real GitHub resource through a Crossplane MR.
3. Make a change
Edit the Repository to add a topic, and watch GitHub reflect the change:
kubectl patch repository.repo.github.upbound.io pair-<your-pair-id>-hello \
--type merge -p '{"spec":{"forProvider":{"topics":["crossplane-workshop"]}}}'
Refresh the GitHub page — the topic appears. Same reconcile loop as every other Crossplane MR; the only difference is what's on the other side of the API.
6.5 What just happened
You proved that Crossplane MRs can drive a SaaS API the same way they drive Kubernetes resources. The mechanics — Provider, ProviderConfig, MR with a forProvider spec, Ready/Synced conditions — are identical. The only new thing is that the credential lives in a Secret your ProviderConfig references, instead of being implicit in a ServiceAccount token. That same shape works for any provider that takes a Secret-based credential — AWS, GCP, Azure, Vault, Datadog, you name it.
Go deeper
- provider-upjet-github — full list of resources (Team, Membership, BranchProtection, …) and ProviderConfig auth modes.
- GitHub App authentication — what an installation token is, why it expires hourly, and how the provider refreshes it for you.