@async/github-app 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API_SURFACE.md +37 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/api-contract.json +115 -0
- package/dist/actions.d.ts +49 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +190 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +42 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +101 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +85 -0
- package/dist/content.d.ts +42 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +269 -0
- package/dist/github.d.ts +11 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +258 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/safety.d.ts +7 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +57 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +111 -0
- package/dist/types.d.ts +177 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +7 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +22 -0
- package/package.json +84 -0
package/API_SURFACE.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @async/github-app API Surface Ledger
|
|
2
|
+
|
|
3
|
+
This file is the generated review ledger for semantic API contract features. It is current-state contract documentation, not a changelog or tutorial.
|
|
4
|
+
|
|
5
|
+
## Async GitHub App Package Exports
|
|
6
|
+
|
|
7
|
+
Contract: `@async/github-app.package`
|
|
8
|
+
|
|
9
|
+
### Exports
|
|
10
|
+
|
|
11
|
+
| Feature | Title | Release | Stability | Lifecycle | Replacement | Docs |
|
|
12
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
13
|
+
| `export.actions` | @async/github-app/actions exports GitHub Actions bridge workflow rendering and pull-based apply helpers | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
14
|
+
| `export.content` | @async/github-app/content exports JSON, JSONC, Markdown, MDX, and generic content mapping helpers | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
15
|
+
| `export.root` | @async/github-app exports auth providers, GitHub client operations, app metadata, change-set types, receipts, and safety helpers | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
16
|
+
| `export.server` | @async/github-app/server exports Fetch-compatible webhook verification and routing handlers | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
17
|
+
|
|
18
|
+
## Async GitHub Integration Runtime
|
|
19
|
+
|
|
20
|
+
Contract: `@async/github-app.runtime`
|
|
21
|
+
|
|
22
|
+
### Runtime
|
|
23
|
+
|
|
24
|
+
| Feature | Title | Release | Stability | Lifecycle | Replacement | Docs |
|
|
25
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
26
|
+
| `runtime.actions-bridge` | Actions bridge mode renders workflow YAML and pulls approved change sets with repo-local GITHUB_TOKEN receipts, lease ids, branch-prefix checks, and allowed-path checks | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
27
|
+
| `runtime.auth` | Auth providers support GitHub App installation tokens, user tokens, static tokens, and Actions GITHUB_TOKEN fallback | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
28
|
+
| `runtime.change-set` | Change sets validate safe paths and commit upserts or deletes serially with branch, commit, PR, and index receipt metadata | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
29
|
+
| `runtime.content` | Content helpers map records to JSON, JSONC read-only-by-default, Markdown, and MDX file formats without schema ownership | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
30
|
+
| `runtime.webhook` | Webhook handlers verify SHA-256 signatures before JSON routing and treat duplicate delivery ids idempotently | public | preview | active | | [docs](https://github.com/async/github-app/blob/main/README.md) |
|
|
31
|
+
|
|
32
|
+
## Supported Surfaces
|
|
33
|
+
|
|
34
|
+
| Contract | Hash | Features |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `@async/github-app.package` | `sha256:9417af3e2ab66056111e0963fb92c2142a0ddba2fd53de1fb981a78af43ddf42` | `export.actions`, `export.content`, `export.root`, `export.server` |
|
|
37
|
+
| `@async/github-app.runtime` | `sha256:f21bef8556025acf31d8cfe0e22e12beefe9c7a8f50b06bd59a39c651d8cacdc` | `runtime.actions-bridge`, `runtime.auth`, `runtime.change-set`, `runtime.content`, `runtime.webhook` |
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1 - 2026-06-18
|
|
4
|
+
|
|
5
|
+
- Add branch-prefix, allowed-path, and pull-request controls to the Actions bridge renderer and pull command so generated workflows can scope repo writes without changing backend APIs.
|
|
6
|
+
- Skip change sets whose metadata excludes the Actions worker, reject bridge pulls when the queued target branch is outside the configured prefix, and echo backend lease ids in bridge receipts.
|
|
7
|
+
|
|
8
|
+
## 0.1.0 - 2026-06-17
|
|
9
|
+
|
|
10
|
+
- Initial GitHub integration package for Async.
|
|
11
|
+
- Adds GitHub App, token, and Actions bridge auth providers.
|
|
12
|
+
- Adds branch, file commit, pull request, tree snapshot, compare, webhook, receipt, and content mapping helpers.
|
|
13
|
+
- Adds JSON, JSONC read-only by default, Markdown, and MDX content serializers.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Async
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# @async/github-app
|
|
2
|
+
|
|
3
|
+
Reusable GitHub integration layer for Async packages.
|
|
4
|
+
|
|
5
|
+
It supports two operating modes:
|
|
6
|
+
|
|
7
|
+
- **GitHub App mode** for the normal SaaS path. Use the Async-owned app metadata by default, or pass a consumer-owned app definition with `defineGithubApp`.
|
|
8
|
+
- **GitHub Actions bridge mode** for organizations that cannot approve a GitHub App installation. The repo installs a generated workflow and uses its own `GITHUB_TOKEN`.
|
|
9
|
+
|
|
10
|
+
The package is content-format agnostic. JSON, JSONC read/index support, Markdown, and MDX use the same branch, commit, pull request, webhook, and receipt machinery.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @async/github-app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Node.js 24 or newer.
|
|
19
|
+
|
|
20
|
+
## Package Exports
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
asyncGithubApp,
|
|
25
|
+
createGitHubClient,
|
|
26
|
+
defineGithubApp,
|
|
27
|
+
githubAppAuth
|
|
28
|
+
} from "@async/github-app";
|
|
29
|
+
|
|
30
|
+
import { createGithubWebhookHandler } from "@async/github-app/server";
|
|
31
|
+
import { renderActionsBridgeWorkflow } from "@async/github-app/actions";
|
|
32
|
+
import { contentMapping, renderJsonContent } from "@async/github-app/content";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## GitHub App Mode
|
|
36
|
+
|
|
37
|
+
The Async-owned app metadata is exported for product wiring:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { asyncGithubApp } from "@async/github-app";
|
|
41
|
+
|
|
42
|
+
console.log(asyncGithubApp.installUrl);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Use installation auth at runtime:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { createGitHubClient, githubAppAuth } from "@async/github-app";
|
|
49
|
+
|
|
50
|
+
const auth = githubAppAuth({
|
|
51
|
+
appId: process.env.GITHUB_APP_ID,
|
|
52
|
+
privateKey: process.env.GITHUB_APP_PRIVATE_KEY,
|
|
53
|
+
installationId: process.env.GITHUB_INSTALLATION_ID
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const github = createGitHubClient(auth);
|
|
57
|
+
|
|
58
|
+
await github.ensureBranch({
|
|
59
|
+
repo: "acme/site",
|
|
60
|
+
from: "main",
|
|
61
|
+
branch: "async/update-homepage"
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const receipt = await github.commitChangeSet({
|
|
65
|
+
repo: "acme/site",
|
|
66
|
+
branch: "async/update-homepage",
|
|
67
|
+
baseBranch: "main",
|
|
68
|
+
message: "Update homepage content",
|
|
69
|
+
files: [
|
|
70
|
+
{
|
|
71
|
+
path: "content/settings.json",
|
|
72
|
+
action: "upsert",
|
|
73
|
+
content: renderJsonContent({ title: "Hello" })
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
allowedPathGlobs: ["content/**"]
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Do not commit private keys, webhook secrets, installation tokens, PATs, or customer tokens. This package never ships Async-owned credentials.
|
|
81
|
+
|
|
82
|
+
Consumers can bring their own app definition:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { defineGithubApp } from "@async/github-app";
|
|
86
|
+
|
|
87
|
+
export const customerApp = defineGithubApp({
|
|
88
|
+
metadata: {
|
|
89
|
+
slug: "acme-content-app",
|
|
90
|
+
installUrl: "https://github.com/apps/acme-content-app/installations/new",
|
|
91
|
+
callbackUrl: "https://acme.example/github/callback"
|
|
92
|
+
},
|
|
93
|
+
permissions: {
|
|
94
|
+
contents: "write",
|
|
95
|
+
metadata: "read",
|
|
96
|
+
pull_requests: "write"
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Webhooks
|
|
102
|
+
|
|
103
|
+
`@async/github-app/server` exports Fetch-compatible handlers that work in Workers-style runtimes and can be adapted to Node HTTP.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { createGithubWebhookHandler } from "@async/github-app/server";
|
|
107
|
+
|
|
108
|
+
export default {
|
|
109
|
+
fetch: createGithubWebhookHandler({
|
|
110
|
+
verify: { secret: process.env.GITHUB_WEBHOOK_SECRET },
|
|
111
|
+
route: {
|
|
112
|
+
push: async (event) => {
|
|
113
|
+
await queueReindex(event.payload);
|
|
114
|
+
},
|
|
115
|
+
pull_request: async (event) => {
|
|
116
|
+
await queueReindex(event.payload);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The handler verifies `X-Hub-Signature-256` before parsing trusted JSON, limits body size, and treats duplicate GitHub delivery IDs as idempotent.
|
|
124
|
+
|
|
125
|
+
## GitHub Actions Bridge Mode
|
|
126
|
+
|
|
127
|
+
For organizations that cannot approve a GitHub App install, render a repo-local workflow:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { renderActionsBridgeWorkflow } from "@async/github-app/actions";
|
|
131
|
+
|
|
132
|
+
const yaml = renderActionsBridgeWorkflow({
|
|
133
|
+
asyncEndpoint: "${{ vars.ASYNC_PROJECT_URL }}",
|
|
134
|
+
branchPrefix: "async/bridge/",
|
|
135
|
+
allowedPathGlobs: ["pipeline.ts", "package.json", "docs/**"]
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Prefer `@async/pipeline` generated workflows for new repos so workflow triggers,
|
|
140
|
+
permissions, action pins, locks, and secret routing stay centrally managed. The
|
|
141
|
+
standalone renderer remains available for compatibility.
|
|
142
|
+
|
|
143
|
+
The generated workflow:
|
|
144
|
+
|
|
145
|
+
- supports `workflow_dispatch`
|
|
146
|
+
- runs on a documented five-minute schedule by default
|
|
147
|
+
- requests `contents: write` and `pull-requests: write`
|
|
148
|
+
- uses `ASYNC_PROJECT_TOKEN` plus repo-local `GITHUB_TOKEN`
|
|
149
|
+
- pulls approved change sets from Async
|
|
150
|
+
- enforces configured branch-prefix and allowed-path constraints
|
|
151
|
+
- commits branches and optionally opens PRs
|
|
152
|
+
- posts lease-aware receipts back to Async
|
|
153
|
+
|
|
154
|
+
Repo setting required for PR creation: enable “Allow GitHub Actions to create and approve pull requests”. If that is unavailable, Async can use branch-only mode and let a human open the PR.
|
|
155
|
+
|
|
156
|
+
External dispatch is optional. Async can trigger `workflow_dispatch` only when the customer provides a token with Actions write permission. Without that token, schedule or manual run is the fallback.
|
|
157
|
+
|
|
158
|
+
## Content Helpers
|
|
159
|
+
|
|
160
|
+
JSON writes are canonical and stable:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { renderJsonContent } from "@async/github-app/content";
|
|
164
|
+
|
|
165
|
+
const content = renderJsonContent({ enabled: true });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
JSONC is readable by default, but writes are opt-in because comments and formatting cannot be preserved safely:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { parseJsoncContent } from "@async/github-app/content";
|
|
172
|
+
|
|
173
|
+
const value = parseJsoncContent(`{
|
|
174
|
+
// allowed on read
|
|
175
|
+
"enabled": true,
|
|
176
|
+
}`);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Markdown and MDX helpers preserve body text and use frontmatter for record fields:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { parseMarkdownRecord, renderMarkdownRecord } from "@async/github-app/content";
|
|
183
|
+
|
|
184
|
+
const record = parseMarkdownRecord("---\ntitle: \"Hello\"\n---\nBody text\n");
|
|
185
|
+
const file = renderMarkdownRecord(record);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Generic mappings let future `@async/db` integration point resources at files without hard-coding formats into GitHub auth:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { contentMapping } from "@async/github-app/content";
|
|
192
|
+
|
|
193
|
+
const posts = contentMapping({
|
|
194
|
+
resource: "posts",
|
|
195
|
+
pattern: "content/posts/{id}.json",
|
|
196
|
+
format: "json"
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const path = posts.pathFromRecord({ id: "hello", title: "Hello" });
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Safety Defaults
|
|
203
|
+
|
|
204
|
+
Change-set paths are rejected when they are absolute, include `..`, include empty segments, duplicate another file in the same change set, or write `.github/workflows/**` without `allowWorkflowPaths`.
|
|
205
|
+
|
|
206
|
+
Use `allowedPathGlobs` to constrain writes:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
await github.commitChangeSet({
|
|
210
|
+
repo: "acme/site",
|
|
211
|
+
branch: "async/content",
|
|
212
|
+
message: "Update content",
|
|
213
|
+
files,
|
|
214
|
+
allowedPathGlobs: ["content/**", "docs/**"]
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Receipts include commit SHAs, branch names, PR URLs, file paths, and index hints. They do not include file contents.
|
|
219
|
+
|
|
220
|
+
## Verification
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pnpm install
|
|
224
|
+
pnpm run release:check
|
|
225
|
+
npm pack --dry-run
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
CI is generated from `pipeline.ts` by `@async/pipeline`; workflow YAML should not be hand-edited.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "api-contract.package.v1",
|
|
3
|
+
"packageName": "@async/github-app",
|
|
4
|
+
"catalogs": [
|
|
5
|
+
{
|
|
6
|
+
"format": "api-contract.catalog.v1",
|
|
7
|
+
"contractId": "@async/github-app.package",
|
|
8
|
+
"title": "Async GitHub App Package Exports",
|
|
9
|
+
"features": [
|
|
10
|
+
{
|
|
11
|
+
"id": "export.root",
|
|
12
|
+
"title": "@async/github-app exports auth providers, GitHub client operations, app metadata, change-set types, receipts, and safety helpers",
|
|
13
|
+
"releaseTag": "public",
|
|
14
|
+
"stability": "preview",
|
|
15
|
+
"group": "exports",
|
|
16
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "export.server",
|
|
20
|
+
"title": "@async/github-app/server exports Fetch-compatible webhook verification and routing handlers",
|
|
21
|
+
"releaseTag": "public",
|
|
22
|
+
"stability": "preview",
|
|
23
|
+
"group": "exports",
|
|
24
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "export.actions",
|
|
28
|
+
"title": "@async/github-app/actions exports GitHub Actions bridge workflow rendering and pull-based apply helpers",
|
|
29
|
+
"releaseTag": "public",
|
|
30
|
+
"stability": "preview",
|
|
31
|
+
"group": "exports",
|
|
32
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "export.content",
|
|
36
|
+
"title": "@async/github-app/content exports JSON, JSONC, Markdown, MDX, and generic content mapping helpers",
|
|
37
|
+
"releaseTag": "public",
|
|
38
|
+
"stability": "preview",
|
|
39
|
+
"group": "exports",
|
|
40
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"format": "api-contract.catalog.v1",
|
|
46
|
+
"contractId": "@async/github-app.runtime",
|
|
47
|
+
"title": "Async GitHub Integration Runtime",
|
|
48
|
+
"features": [
|
|
49
|
+
{
|
|
50
|
+
"id": "runtime.auth",
|
|
51
|
+
"title": "Auth providers support GitHub App installation tokens, user tokens, static tokens, and Actions GITHUB_TOKEN fallback",
|
|
52
|
+
"releaseTag": "public",
|
|
53
|
+
"stability": "preview",
|
|
54
|
+
"group": "runtime",
|
|
55
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "runtime.change-set",
|
|
59
|
+
"title": "Change sets validate safe paths and commit upserts or deletes serially with branch, commit, PR, and index receipt metadata",
|
|
60
|
+
"releaseTag": "public",
|
|
61
|
+
"stability": "preview",
|
|
62
|
+
"group": "runtime",
|
|
63
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "runtime.webhook",
|
|
67
|
+
"title": "Webhook handlers verify SHA-256 signatures before JSON routing and treat duplicate delivery ids idempotently",
|
|
68
|
+
"releaseTag": "public",
|
|
69
|
+
"stability": "preview",
|
|
70
|
+
"group": "runtime",
|
|
71
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "runtime.actions-bridge",
|
|
75
|
+
"title": "Actions bridge mode renders workflow YAML and pulls approved change sets with repo-local GITHUB_TOKEN receipts, lease ids, branch-prefix checks, and allowed-path checks",
|
|
76
|
+
"releaseTag": "public",
|
|
77
|
+
"stability": "preview",
|
|
78
|
+
"group": "runtime",
|
|
79
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "runtime.content",
|
|
83
|
+
"title": "Content helpers map records to JSON, JSONC read-only-by-default, Markdown, and MDX file formats without schema ownership",
|
|
84
|
+
"releaseTag": "public",
|
|
85
|
+
"stability": "preview",
|
|
86
|
+
"group": "runtime",
|
|
87
|
+
"docsUrl": "https://github.com/async/github-app/blob/main/README.md"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"supported": [
|
|
93
|
+
{
|
|
94
|
+
"format": "api-contract.surface.v1",
|
|
95
|
+
"contractId": "@async/github-app.package",
|
|
96
|
+
"features": [
|
|
97
|
+
"export.root",
|
|
98
|
+
"export.server",
|
|
99
|
+
"export.actions",
|
|
100
|
+
"export.content"
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"format": "api-contract.surface.v1",
|
|
105
|
+
"contractId": "@async/github-app.runtime",
|
|
106
|
+
"features": [
|
|
107
|
+
"runtime.auth",
|
|
108
|
+
"runtime.actions-bridge",
|
|
109
|
+
"runtime.change-set",
|
|
110
|
+
"runtime.content",
|
|
111
|
+
"runtime.webhook"
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ChangeSet, CommitReceipt, GitHubAuthProvider } from "./types.js";
|
|
2
|
+
export interface RenderActionsBridgeWorkflowOptions {
|
|
3
|
+
readonly name?: string;
|
|
4
|
+
readonly asyncEndpoint?: string;
|
|
5
|
+
readonly packageVersion?: string;
|
|
6
|
+
readonly schedule?: string;
|
|
7
|
+
readonly includePushTrigger?: boolean;
|
|
8
|
+
readonly nodeVersion?: string | number;
|
|
9
|
+
readonly pnpmVersion?: string;
|
|
10
|
+
readonly branchPrefix?: string;
|
|
11
|
+
readonly allowedPathGlobs?: readonly string[];
|
|
12
|
+
readonly pullRequest?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface PendingChangeSetsResponse {
|
|
15
|
+
readonly changeSets: readonly ChangeSet[];
|
|
16
|
+
readonly leases?: readonly ActionsBridgeLease[];
|
|
17
|
+
}
|
|
18
|
+
export interface ActionsBridgeLease {
|
|
19
|
+
readonly changeSetId: string;
|
|
20
|
+
readonly repo: string;
|
|
21
|
+
readonly worker: "actions" | "app";
|
|
22
|
+
readonly leaseId: string;
|
|
23
|
+
readonly leaseExpiresAt?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ApplyActionsBridgeOptions {
|
|
26
|
+
readonly endpoint: string;
|
|
27
|
+
readonly projectToken: string;
|
|
28
|
+
readonly repository: string;
|
|
29
|
+
readonly auth?: GitHubAuthProvider;
|
|
30
|
+
readonly fetch?: typeof fetch;
|
|
31
|
+
readonly requireApproved?: boolean;
|
|
32
|
+
readonly branchPrefix?: string;
|
|
33
|
+
readonly allowedPathGlobs?: readonly string[];
|
|
34
|
+
readonly pullRequest?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ApplyActionsBridgeResult {
|
|
37
|
+
readonly receipts: readonly ActionsBridgeReceipt[];
|
|
38
|
+
readonly skipped: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ActionsBridgeReceipt extends CommitReceipt {
|
|
41
|
+
readonly changeSetId: string;
|
|
42
|
+
readonly leaseId?: string;
|
|
43
|
+
readonly leaseExpiresAt?: string;
|
|
44
|
+
readonly worker: "actions";
|
|
45
|
+
readonly status: "applied";
|
|
46
|
+
}
|
|
47
|
+
export declare function renderActionsBridgeWorkflow(options?: RenderActionsBridgeWorkflowOptions): string;
|
|
48
|
+
export declare function applyActionsBridge(options: ApplyActionsBridgeOptions): Promise<ApplyActionsBridgeResult>;
|
|
49
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAmB,MAAM,YAAY,CAAC;AAGhG,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAED,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,kCAAuC,GAAG,MAAM,CA6CpG;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAuE9G"}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { actionsBridgeAuth } from "./auth.js";
|
|
2
|
+
import { createGitHubClient } from "./github.js";
|
|
3
|
+
import { redactSensitive } from "./util.js";
|
|
4
|
+
export function renderActionsBridgeWorkflow(options = {}) {
|
|
5
|
+
const name = options.name ?? "Async GitHub Bridge";
|
|
6
|
+
const asyncEndpoint = options.asyncEndpoint ?? "${{ vars.ASYNC_PROJECT_URL }}";
|
|
7
|
+
const packageVersion = options.packageVersion ?? "latest";
|
|
8
|
+
const schedule = options.schedule ?? "*/5 * * * *";
|
|
9
|
+
const nodeVersion = String(options.nodeVersion ?? 24);
|
|
10
|
+
const pnpmVersion = options.pnpmVersion ?? "10.20.0";
|
|
11
|
+
const pushTrigger = options.includePushTrigger ? "\n push:\n branches:\n - main\n" : "";
|
|
12
|
+
const pullArgs = renderActionsPullArgs({
|
|
13
|
+
branchPrefix: options.branchPrefix,
|
|
14
|
+
allowedPathGlobs: options.allowedPathGlobs,
|
|
15
|
+
pullRequest: options.pullRequest
|
|
16
|
+
});
|
|
17
|
+
return `name: ${name}
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
schedule:
|
|
22
|
+
- cron: "${schedule}"${pushTrigger}
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: write
|
|
26
|
+
pull-requests: write
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
bridge:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: pnpm/action-setup@v4
|
|
34
|
+
with:
|
|
35
|
+
version: ${pnpmVersion}
|
|
36
|
+
- uses: actions/setup-node@v4
|
|
37
|
+
with:
|
|
38
|
+
node-version: ${nodeVersion}
|
|
39
|
+
cache: pnpm
|
|
40
|
+
- name: Pull and apply Async change sets
|
|
41
|
+
run: pnpm dlx @async/github-app@${packageVersion} actions pull${pullArgs}
|
|
42
|
+
env:
|
|
43
|
+
ASYNC_PROJECT_URL: ${asyncEndpoint}
|
|
44
|
+
ASYNC_PROJECT_TOKEN: \${{ secrets.ASYNC_PROJECT_TOKEN }}
|
|
45
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
46
|
+
GITHUB_REPOSITORY: \${{ github.repository }}
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
export async function applyActionsBridge(options) {
|
|
50
|
+
const apiFetch = options.fetch ?? fetch;
|
|
51
|
+
const auth = options.auth ?? actionsBridgeAuth();
|
|
52
|
+
const client = createGitHubClient(auth);
|
|
53
|
+
const pending = await requestJson(apiFetch, `${trimSlash(options.endpoint)}/github/actions-bridge/change-sets?repo=${encodeURIComponent(options.repository)}`, {
|
|
54
|
+
method: "GET",
|
|
55
|
+
token: options.projectToken
|
|
56
|
+
});
|
|
57
|
+
const leases = indexLeases(pending.leases);
|
|
58
|
+
const receipts = [];
|
|
59
|
+
let skipped = 0;
|
|
60
|
+
for (const changeSet of pending.changeSets) {
|
|
61
|
+
if ((options.requireApproved ?? true) && changeSet.metadata?.approved !== true) {
|
|
62
|
+
skipped += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!isAllowedActionsWorker(changeSet.metadata)) {
|
|
66
|
+
skipped += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const lease = leases.get(changeSet.id) ?? leaseFromMetadata(changeSet);
|
|
70
|
+
if (lease && lease.worker !== "actions") {
|
|
71
|
+
skipped += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (lease && lease.repo !== formatRepoInput(changeSet.repo)) {
|
|
75
|
+
throw new Error(`Actions bridge rejected change set ${changeSet.id}: lease repo ${lease.repo} does not match change set repo ${formatRepoInput(changeSet.repo)}.`);
|
|
76
|
+
}
|
|
77
|
+
if (options.branchPrefix && !changeSet.targetBranch.startsWith(options.branchPrefix)) {
|
|
78
|
+
throw new Error(`Actions bridge rejected change set ${changeSet.id}: target branch must start with ${options.branchPrefix}.`);
|
|
79
|
+
}
|
|
80
|
+
const receipt = toActionsBridgeReceipt(await client.commitChangeSet({
|
|
81
|
+
repo: changeSet.repo,
|
|
82
|
+
branch: changeSet.targetBranch,
|
|
83
|
+
baseBranch: changeSet.baseBranch,
|
|
84
|
+
changeSetId: changeSet.id,
|
|
85
|
+
message: changeSet.message ?? `Apply Async change set ${changeSet.id}`,
|
|
86
|
+
files: changeSet.files,
|
|
87
|
+
allowedPathGlobs: options.allowedPathGlobs,
|
|
88
|
+
metadata: changeSet.metadata
|
|
89
|
+
}), changeSet, lease);
|
|
90
|
+
receipts.push(receipt);
|
|
91
|
+
if (options.pullRequest !== false && (changeSet.mode === "pull_request" || changeSet.mode === "actions-pull")) {
|
|
92
|
+
const pr = await client.openOrUpdatePullRequest({
|
|
93
|
+
repo: changeSet.repo,
|
|
94
|
+
head: changeSet.targetBranch,
|
|
95
|
+
base: changeSet.baseBranch,
|
|
96
|
+
title: changeSet.title ?? `Apply Async change set ${changeSet.id}`,
|
|
97
|
+
body: changeSet.body ?? "Created by the Async GitHub Actions bridge."
|
|
98
|
+
});
|
|
99
|
+
receipts[receipts.length - 1] = {
|
|
100
|
+
...receipt,
|
|
101
|
+
pullRequestUrl: pr.url
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await requestJson(apiFetch, `${trimSlash(options.endpoint)}/github/actions-bridge/receipts`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
token: options.projectToken,
|
|
108
|
+
body: {
|
|
109
|
+
repository: options.repository,
|
|
110
|
+
receipts,
|
|
111
|
+
skipped
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return { receipts, skipped };
|
|
115
|
+
}
|
|
116
|
+
function toActionsBridgeReceipt(receipt, changeSet, lease) {
|
|
117
|
+
return {
|
|
118
|
+
...receipt,
|
|
119
|
+
changeSetId: changeSet.id,
|
|
120
|
+
...(lease ? { leaseId: lease.leaseId } : {}),
|
|
121
|
+
...(lease?.leaseExpiresAt ? { leaseExpiresAt: lease.leaseExpiresAt } : {}),
|
|
122
|
+
worker: "actions",
|
|
123
|
+
status: "applied"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function indexLeases(leases) {
|
|
127
|
+
const indexed = new Map();
|
|
128
|
+
for (const lease of leases ?? []) {
|
|
129
|
+
indexed.set(lease.changeSetId, lease);
|
|
130
|
+
}
|
|
131
|
+
return indexed;
|
|
132
|
+
}
|
|
133
|
+
function leaseFromMetadata(changeSet) {
|
|
134
|
+
const leaseId = changeSet.metadata?.leaseId;
|
|
135
|
+
if (typeof leaseId !== "string" || !leaseId)
|
|
136
|
+
return undefined;
|
|
137
|
+
const worker = changeSet.metadata?.worker === "app" ? "app" : "actions";
|
|
138
|
+
const leaseExpiresAt = typeof changeSet.metadata.leaseExpiresAt === "string" ? changeSet.metadata.leaseExpiresAt : undefined;
|
|
139
|
+
return {
|
|
140
|
+
changeSetId: changeSet.id,
|
|
141
|
+
repo: formatRepoInput(changeSet.repo),
|
|
142
|
+
worker,
|
|
143
|
+
leaseId,
|
|
144
|
+
...(leaseExpiresAt ? { leaseExpiresAt } : {})
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function formatRepoInput(repo) {
|
|
148
|
+
return typeof repo === "string" ? repo : `${repo.owner}/${repo.repo}`;
|
|
149
|
+
}
|
|
150
|
+
function renderActionsPullArgs(options) {
|
|
151
|
+
const args = [];
|
|
152
|
+
if (options.branchPrefix)
|
|
153
|
+
args.push("--branch-prefix", options.branchPrefix);
|
|
154
|
+
if (options.pullRequest !== undefined)
|
|
155
|
+
args.push("--pull-request", String(options.pullRequest));
|
|
156
|
+
for (const glob of options.allowedPathGlobs ?? []) {
|
|
157
|
+
args.push("--allowed-path", glob);
|
|
158
|
+
}
|
|
159
|
+
return args.length > 0 ? ` ${args.map(shellWord).join(" ")}` : "";
|
|
160
|
+
}
|
|
161
|
+
function isAllowedActionsWorker(metadata) {
|
|
162
|
+
const allowedWorkers = metadata?.allowedWorkers;
|
|
163
|
+
if (!Array.isArray(allowedWorkers))
|
|
164
|
+
return true;
|
|
165
|
+
return allowedWorkers.includes("actions");
|
|
166
|
+
}
|
|
167
|
+
async function requestJson(apiFetch, url, options) {
|
|
168
|
+
const init = {
|
|
169
|
+
method: options.method,
|
|
170
|
+
headers: {
|
|
171
|
+
accept: "application/json",
|
|
172
|
+
authorization: `Bearer ${options.token}`,
|
|
173
|
+
"content-type": "application/json"
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
if (options.body !== undefined) {
|
|
177
|
+
init.body = JSON.stringify(options.body);
|
|
178
|
+
}
|
|
179
|
+
const response = await apiFetch(url, init);
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(`Async Actions bridge request failed with ${response.status}: ${redactSensitive(await response.text())}`);
|
|
182
|
+
}
|
|
183
|
+
return await response.json();
|
|
184
|
+
}
|
|
185
|
+
function trimSlash(value) {
|
|
186
|
+
return value.replace(/\/+$/u, "");
|
|
187
|
+
}
|
|
188
|
+
function shellWord(value) {
|
|
189
|
+
return /^[A-Za-z0-9_./:@*-]+$/u.test(value) ? value : JSON.stringify(value);
|
|
190
|
+
}
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AsyncGithubAppMetadata, DefineGithubAppOptions, GithubAppDefinition } from "./types.js";
|
|
2
|
+
export declare const asyncGithubApp: AsyncGithubAppMetadata;
|
|
3
|
+
export declare function defineGithubApp(options?: DefineGithubAppOptions): GithubAppDefinition;
|
|
4
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtG,eAAO,MAAM,cAAc,EAAE,sBAiB5B,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,mBAAmB,CAwBzF"}
|