Skip to main content

Create resources in GitHub ⏱️ 20m

Your pair:
Running solo locally?

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 with pair-<your-id>- belong to someone else.
  • No deletes. The App is intentionally not granted delete_repo, so a stray kubectl delete repository.repo.github.upbound.io will 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.

Missing the Secret?

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.