Skip to content

Repositories

GitHub.Repository manages the lifecycle of a repository owned by a user or organization. The repository is created on first deploy and its settings are converged on every subsequent deploy — description, visibility, tabs, merge policy, topics — so repo configuration lives in code next to the infrastructure it hosts.

Repositories are treated as precious: destroying the stack retains the repository by default, and deleting it is an explicit opt-in. New here? Set up credentials first.

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as GitHub from "alchemy/GitHub";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyRepos",
{
providers: GitHub.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const repo = yield* GitHub.Repository("api", {
owner: "my-org",
name: "api",
description: "API service",
autoInit: true,
});
return { url: repo.htmlUrl, clone: repo.cloneUrl };
}),
);

owner is a user or organization login — alchemy picks the matching create endpoint automatically. Outputs include the stable numeric repoId, fullName, htmlUrl, the sshUrl/cloneUrl/gitUrl clone URLs, and the resolved defaultBranch.

Pointing Repository at a name that already exists under the owner doesn’t fail with a name conflict. The deploy observes the live repository and converges its settings in place, so you can bring an existing repo under management just by declaring it:

yield* GitHub.Repository("api", {
owner: "my-org",
name: "api", // already exists — settings below are applied to it
hasWiki: false,
deleteBranchOnMerge: true,
});

Every deploy converges the declared settings against the live repository:

const repo = yield* GitHub.Repository("internal-tools", {
owner: "my-org",
name: "internal-tools",
visibility: "private",
hasWiki: false,
hasProjects: false,
deleteBranchOnMerge: true,
});
  • description, homepage — the repository page blurb and link.
  • visibility"public" (default), "private", or "internal" (organization repos on GitHub Enterprise only).
  • TabshasIssues, hasProjects, hasWiki (default true) and hasDiscussions (default false).
  • Merge policyallowSquashMerge, allowMergeCommit, allowRebaseMerge (default true), allowAutoMerge and deleteBranchOnMerge (default false).
  • isTemplate — mark the repository as a template.
  • defaultBranch — only applied once the branch actually exists; on an empty repository it’s dropped for that deploy and applied on a later one.

The topics list fully replaces whatever is on the repository — removing the prop on a later deploy clears them:

yield* GitHub.Repository("sdk", {
owner: "my-org",
name: "sdk",
topics: ["typescript", "effect", "sdk"],
});

autoInit, gitignoreTemplate, and licenseTemplate initialize the repository at create time — an empty README, a .gitignore template, and a license. They are only honored on creation; changing them on a later deploy has no effect on an existing repository:

yield* GitHub.Repository("service", {
owner: "my-org",
name: "service",
autoInit: true,
gitignoreTemplate: "Node",
licenseTemplate: "mit",
});

Keep the same logical ID and change name to rename the live repository in place — history, issues, and pull requests are preserved, and the numeric repoId stays the same:

// First deploy creates "api"; this later deploy (same logical ID)
// renames it to "gateway".
const repo = yield* GitHub.Repository("api", {
owner: "my-org",
name: "gateway",
});

Only changing owner triggers a replacement — a repository can’t be moved between owners by an update.

archived: true makes the repository read-only; set it back to false on a later deploy to un-archive:

yield* GitHub.Repository("legacy", {
owner: "my-org",
name: "legacy-service",
archived: true,
});

Destroying the stack does not delete the repository — the default removal policy is retain, protecting irreplaceable history. Opt into actual deletion with destroy():

import { destroy } from "alchemy/RemovalPolicy";
yield* GitHub.Repository("ephemeral", {
owner: "my-org",
name: "ephemeral-preview",
}).pipe(destroy());

The token needs the delete_repo scope for this (plain repo scope covers everything else). Deletion resolves the repository by its stable repoId, so it targets the right repo even after a rename.

For anything the resource model doesn’t cover, the GitHubCredentials service hands you an authenticated Octokit client — the same one every GitHub resource uses:

import * as GitHub from "alchemy/GitHub";
import * as Effect from "effect/Effect";
const program = Effect.gen(function* () {
const creds = yield* yield* GitHub.GitHubCredentials;
const octokit = creds.octokit();
const { data } = yield* Effect.tryPromise(() =>
octokit.rest.repos.get({ owner: "my-org", repo: "api" }),
);
return data;
});

(GitHubCredentials resolves to an effect that yields the service, hence the double yield*.)

Inside a stack, GitHub.providers() already carries GitHubCredentials, resolved via the login flow. Outside one, build the layer directly:

  • GitHub.fromEnv() — reads GITHUB_ACCESS_TOKEN or GITHUB_TOKEN.
  • GitHub.fromToken(token) — a literal token, plain string or Redacted — useful in tests.
program.pipe(Effect.provide(GitHub.fromEnv()));

Related:

Reference: