1---
2title: "Part 3: Publish a repository"
3description: Allow anonymous clones of public repositories while preserving authenticated writes.
4---
5
6import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
7
8The credential from [Part 2](/git/tutorial/part-2) protects every request.
9Now make `acme/web` readable by anyone while keeping writes protected.
10
11## Mark the repository public
12
13```sh
14curl --fail-with-body -u "x:$GIT_SECRET" \
15 -X PATCH "$HOST/api/v1/repos/acme/web" \
16 -H "Content-Type: application/json" \
17 -d '{"public":true}'
18```
19
20`public` is stored repository metadata. It does not bypass your middleware.
21An anonymous request still receives `401` until you teach the policy to use it.
22
23## Look up the repository's visibility
24
25Create `src/public-read.ts`:
26
27```typescript
28// src/public-read.ts
29import * as Git from "alchemy/Git";
30import * as Effect from "effect/Effect";
31import * as HttpRouter from "effect/http/HttpRouter";
32import { HttpServerRequest } from "effect/http/HttpServerRequest";
33
34export const PublicRead = Effect.gen(function* () {
35 const registry = yield* Git.RegistryStore;
36 return Effect.gen(function* () {
37 const request = yield* HttpServerRequest;
38 if (!Git.isRead(request)) return false;
39 const { owner, repo } = yield* HttpRouter.params;
40 if (owner === undefined || repo === undefined) return false;
41 const entry = yield* registry
42 .resolve(owner.toLowerCase(), repo.toLowerCase().replace(/\.git$/, ""))
43 .pipe(Effect.catchTag("StoreError", () => Effect.succeed(undefined)));
44 return entry?.public === true;
45 });
46});
47```
48
49The outer effect acquires the registry when the routes are built. The inner
50effect checks the current request. It permits only reads of a named, public
51repository. Listing every repository is not a read of one public repository.
52
53`Git.isRead` understands Git's protocol: a clone includes a POST to
54`git-upload-pack`, while discovery for a push must remain protected.
55
56## Permit public reads in the middleware
57
58Import the check:
59
60```diff lang="typescript"
61// src/middleware.ts
62import { GitSecret } from "./secret.ts";
63+import { PublicRead } from "./public-read.ts";
64```
65
66Acquire it alongside the credential:
67
68```diff lang="typescript"
69// src/middleware.ts
70 const secret = yield* (yield* GitSecret).text;
71+ const publicRead = yield* PublicRead;
72```
73
74Before returning `401`, allow requests that pass the public-read check:
75
76```diff lang="typescript"
77// src/middleware.ts
78 if (Redacted.value(password) === Redacted.value(yield* secret)) {
79 return yield* httpEffect;
80 }
81+ if (yield* publicRead) return yield* httpEffect;
82 return HttpServerResponse.empty({
83```
84
85The existing `Git.RegistryDurableObject` layer supplies this new dependency.
86The shared credential still allows all operations; anonymous callers only get
87the public reads you just defined.
88
89## Deploy the policy
90
91<Tabs syncKey="pkgManager">
92 <TabItem label="bun" icon="bun">
93 <Code code={`bun alchemy deploy`} lang="sh" />
94 </TabItem>
95 <TabItem label="npm" icon="npm">
96 <Code code={`npx alchemy deploy`} lang="sh" />
97 </TabItem>
98 <TabItem label="pnpm" icon="pnpm">
99 <Code code={`pnpm alchemy deploy`} lang="sh" />
100 </TabItem>
101 <TabItem label="yarn" icon="seti:yarn">
102 <Code code={`yarn alchemy deploy`} lang="sh" />
103 </TabItem>
104</Tabs>
105
106This updates the Worker. The public flag you set earlier remains stored in the
107registry.
108
109## Clone without credentials
110
111```sh
112GIT_TERMINAL_PROMPT=0 git -c credential.helper= \
113 clone "$HOST/acme/web.git" public-copy
114git -C public-copy fsck --strict
115```
116
117The clone should succeed without a password. Disabling the credential helper
118ensures this check does not accidentally use a saved credential.
119
120## Verify that anonymous writes still fail
121
122```sh
123GIT_TERMINAL_PROMPT=0 git -c credential.helper= -C work \
124 push origin HEAD:anonymous-write
125```
126
127Expect authentication to fail and no `anonymous-write` branch to be created.
128An authenticated push from Part 2 still works.
129
130## Verify that private reads still fail
131
132Temporarily make the repository private:
133
134```sh
135curl --fail-with-body -u "x:$GIT_SECRET" \
136 -X PATCH "$HOST/api/v1/repos/acme/web" \
137 -H "Content-Type: application/json" \
138 -d '{"public":false}'
139curl -i "$HOST/api/v1/repos/acme/web"
140```
141
142The anonymous read must return `401`. Restore the public setting before
143continuing:
144
145```sh
146curl --fail-with-body -u "x:$GIT_SECRET" \
147 -X PATCH "$HOST/api/v1/repos/acme/web" \
148 -H "Content-Type: application/json" \
149 -d '{"public":true}'
150```
151
152[Part 4: Give users their own credentials](/git/tutorial/part-4) replaces the
153shared credential with accounts and individual API keys.