@tahminator/pipeline 1.0.88 → 1.0.89-beta.g8f9210df

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/README.md CHANGED
@@ -30,6 +30,143 @@ bun run src/index.ts
30
30
  > [!WARNING]
31
31
  > This repository is iterating quickly & as such may have rough edges. I will always be happy to respond to & fix any issues anyone may have :)
32
32
 
33
+ ## Slash commands action
34
+
35
+ Use [`actions/commands`](./actions/commands/README.md) to run your repository's workflows from PR comments such as `/merge` or `/ai`.
36
+
37
+ Before using it:
38
+
39
+ - Add the GitHub App ID and private key as repository secrets.
40
+ - Include `yargs` and your loader's dependencies in `.github/scripts/package.json`.
41
+ - Replace `<published-commit-sha>` with the reviewed pipeline commit you want to use.
42
+
43
+ Choose one command source: a hardcoded list or a Bun script.
44
+
45
+ | | Hardcoded | Dynamic |
46
+ | ------------ | -------------------------------------- | ------------------------------------------------ |
47
+ | Input | `COMMANDS` | `DYNAMIC_COMMANDS_BUN_SCRIPT` |
48
+ | Command list | Written directly in the workflow. | Printed to stdout by a Bun script. |
49
+ | Source | Default-branch workflow configuration. | Script from the authorized PR head. |
50
+ | Best for | A fixed list of commands. | Computing the list from repository code or data. |
51
+
52
+ Both use the same permission checks and dispatch to repository-owned command workflows.
53
+
54
+ ### Examples
55
+
56
+ Create these two files to add a `/hello` command that replies to the PR.
57
+
58
+ **`.github/workflows/slash.yml`**
59
+
60
+ ```yaml
61
+ name: Slash Command Dispatch
62
+
63
+ on:
64
+ issue_comment:
65
+ types: [created]
66
+
67
+ permissions:
68
+ contents: read
69
+
70
+ jobs:
71
+ dispatch:
72
+ if: >-
73
+ github.event.issue.pull_request &&
74
+ startsWith(github.event.comment.body, '/')
75
+ runs-on: ubuntu-latest
76
+ steps:
77
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
78
+ with:
79
+ # please set these to keep this workflow secure
80
+ ref: ${{ github.event.repository.default_branch }}
81
+ persist-credentials: false
82
+
83
+ - uses: tahminator/pipeline/actions/setup@<published-commit-sha>
84
+ with:
85
+ LINT_CI: false
86
+
87
+ - uses: tahminator/pipeline/actions/commands@<published-commit-sha>
88
+ with:
89
+ APP_ID: ${{ secrets._GITHUB_APP_APP_ID }}
90
+ PRIVATE_KEY: ${{ secrets._GITHUB_APP_PEM_CONTENT }}
91
+ COMMANDS: |
92
+ hello
93
+ merge
94
+ ```
95
+
96
+ **`.github/workflows/hello-command.yml`**
97
+
98
+ ```yaml
99
+ name: Hello command
100
+
101
+ on:
102
+ workflow_dispatch:
103
+ inputs:
104
+ repository:
105
+ description: Repository where the command was posted
106
+ required: true
107
+ type: string
108
+ comment-id:
109
+ description: ID of the slash-command comment
110
+ required: true
111
+ type: string
112
+ prId:
113
+ description: Pull request number
114
+ required: true
115
+ type: string
116
+ author:
117
+ description: Username that posted the command
118
+ required: true
119
+ type: string
120
+
121
+ permissions:
122
+ pull-requests: write
123
+
124
+ jobs:
125
+ hello:
126
+ runs-on: ubuntu-latest
127
+ steps:
128
+ - name: Reply to the PR
129
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
130
+ with:
131
+ script: |
132
+ const { prId, author } = context.payload.inputs;
133
+ await github.rest.issues.createComment({
134
+ ...context.repo,
135
+ issue_number: Number(prId),
136
+ body: `Hello @${author}! This command ran from \`${context.ref}\`.`,
137
+ });
138
+ ```
139
+
140
+ Once both files are on the default branch, a user with write access can comment `/hello` on an open, same-repository PR. The dispatcher runs `hello-command.yml` from the PR branch, and the handler replies with that branch's name. Replace the reply step with your command's work.
141
+
142
+ ### Commands from a Bun script
143
+
144
+ Keep the same trigger, trusted checkout, and setup steps. Replace the final action step with:
145
+
146
+ ```yaml
147
+ - uses: tahminator/pipeline/actions/commands@<published-commit-sha>
148
+ with:
149
+ APP_ID: ${{ secrets._GITHUB_APP_APP_ID }}
150
+ PRIVATE_KEY: ${{ secrets._GITHUB_APP_PEM_CONTENT }}
151
+ DYNAMIC_COMMANDS_BUN_SCRIPT: .github/scripts/src/load-slash-commands/index.ts
152
+ ```
153
+
154
+ Create that file and print the available command names:
155
+
156
+ ```ts
157
+ const commands = ["merge", "ai", "copy"];
158
+ console.log(commands.join("\n"));
159
+ ```
160
+
161
+ Print only command names to stdout, separated by newlines or commas. Use `console.error` for logs. Empty or invalid output, or a nonzero exit, fails the action.
162
+
163
+ The action checks write access and rejects forks before loading this script from the PR head. Run `setup` first to provide Bun and dependencies; this action installs nothing.
164
+
165
+ > [!IMPORTANT]
166
+ > Do not check out the PR head and install its dependencies before calling this action. The gate cannot protect earlier workflow steps. Keep pre-authorization setup on trusted code, as in the example.
167
+
168
+ See the [action documentation](./actions/commands/README.md) for GitHub App permissions and the full dispatch contract.
169
+
33
170
  ## Available Clients
34
171
 
35
172
  ```ts
@@ -0,0 +1,7 @@
1
+ /** Exactly one source: inline names or a repository-owned Bun loader. */
2
+ export declare function parseCommands(input: string, file: string): {
3
+ commands: string;
4
+ file: string;
5
+ };
6
+ /** Runs only after the action has checked write permission and rejected fork heads. */
7
+ export declare function loadCommands(file: string, workspace: string): Promise<string>;
@@ -0,0 +1,44 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ /** Exactly one source: inline names or a repository-owned Bun loader. */
4
+ export function parseCommands(input, file) {
5
+ if (Boolean(input.trim()) === Boolean(file.trim())) {
6
+ throw new Error("Set exactly one of COMMANDS or DYNAMIC_COMMANDS_BUN_SCRIPT");
7
+ }
8
+ if (file.trim()) {
9
+ if (/[\r\n]/.test(file) ||
10
+ path.isAbsolute(file) ||
11
+ file.split(/[\\/]/).includes("..")) {
12
+ throw new Error("DYNAMIC_COMMANDS_BUN_SCRIPT must be a repository-relative file path");
13
+ }
14
+ return { commands: "", file };
15
+ }
16
+ const commands = input.split(/[\s,]+/).filter(Boolean);
17
+ if (!commands.length ||
18
+ commands.some((name) => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name))) {
19
+ throw new Error("COMMANDS must contain command names separated by commas or newlines");
20
+ }
21
+ return { commands: commands.join("\n"), file: "" };
22
+ }
23
+ /** Runs only after the action has checked write permission and rejected fork heads. */
24
+ export async function loadCommands(file, workspace) {
25
+ const root = await realpath(workspace);
26
+ const source = await realpath(path.resolve(root, file));
27
+ const relative = path.relative(root, source);
28
+ if (relative.startsWith("..") ||
29
+ path.isAbsolute(relative) ||
30
+ !(await stat(source)).isFile()) {
31
+ throw new Error("The command loader must be a file inside the repository");
32
+ }
33
+ const child = Bun.spawn([process.execPath, "run", "--no-install", source], {
34
+ cwd: root,
35
+ stdout: "pipe",
36
+ stderr: "inherit",
37
+ });
38
+ const stdout = await new Response(child.stdout).text();
39
+ const exitCode = await child.exited;
40
+ if (exitCode !== 0) {
41
+ throw new Error(`Command loader exited with code ${exitCode}`);
42
+ }
43
+ return parseCommands(stdout, "").commands;
44
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { appendFile } from "node:fs/promises";
2
+ import yargs from "yargs";
3
+ import { loadCommands, parseCommands } from "./helper";
4
+ const { commands, dynamicCommandsBunScript, file, workspace } = await yargs(process.argv.slice(2))
5
+ .option("commands", {
6
+ type: "string",
7
+ describe: "Hardcoded command names separated by commas or newlines",
8
+ })
9
+ .option("dynamicCommandsBunScript", {
10
+ type: "string",
11
+ describe: "Repository-relative Bun loader path",
12
+ })
13
+ .option("file", {
14
+ type: "string",
15
+ describe: "Execute the previously validated Bun loader",
16
+ })
17
+ .option("workspace", {
18
+ type: "string",
19
+ default: process.env.GITHUB_WORKSPACE,
20
+ describe: "Repository root for loader execution",
21
+ })
22
+ .conflicts("file", ["commands", "dynamicCommandsBunScript"])
23
+ .strict()
24
+ .parse();
25
+ async function main() {
26
+ const output = process.env.GITHUB_OUTPUT;
27
+ if (!output)
28
+ throw new Error("GITHUB_OUTPUT is required");
29
+ let result;
30
+ if (file) {
31
+ if (!workspace)
32
+ throw new Error("--workspace or GITHUB_WORKSPACE is required");
33
+ result = { commands: await loadCommands(file, workspace), file: "" };
34
+ }
35
+ else {
36
+ result = parseCommands(commands ?? "", dynamicCommandsBunScript ?? "");
37
+ }
38
+ await appendFile(output, `commands<<__COMMANDS__\n${result.commands}\n__COMMANDS__\nfile=${result.file}\n`);
39
+ }
40
+ await main();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "type": "module",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A collection of Bun shell scripts that can be re-used in various CICD pipelines.",
6
- "version": "1.0.88",
6
+ "version": "1.0.89-beta.g8f9210df",
7
7
  "repository": {
8
8
  "url": "git+https://github.com/tahminator/pipeline.git"
9
9
  },