grok-telegram-bot 2.2.0 → 2.2.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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/scripts/_push-branch.mjs +23 -33
- package/scripts/_tag-release.mjs +52 -0
- package/src/bot/bot.ts +11 -2
- package/src/bot/menu/status-panel.ts +14 -0
- package/src/bot/permission-service.ts +65 -2
- package/scripts/_release-version.mjs +0 -87
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,14 @@ The latest section is published verbatim as the GitHub Release notes by
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [2.2.1] - 2026-07-12
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **📌 Permission prompts stay pinned** while waiting for Approve/Deny so they
|
|
17
|
+
aren't lost under streaming chat. The pin is removed on approve, deny, or
|
|
18
|
+
timeout, and the status panel is re-pinned afterwards.
|
|
19
|
+
|
|
12
20
|
## [2.2.0] - 2026-07-12
|
|
13
21
|
|
|
14
22
|
The **reliable multi-account** release — account switch / auto-rotate now only
|
|
@@ -669,6 +677,7 @@ from a single chat and switch between them, on a redesigned, compact menu.
|
|
|
669
677
|
diffs, MarkdownV2 rendering, scheduled tasks, multi-image prompts, and a
|
|
670
678
|
cross-platform 24/7 background service.
|
|
671
679
|
|
|
680
|
+
[2.2.1]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.2.1
|
|
672
681
|
[2.2.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.2.0
|
|
673
682
|
[2.1.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.1.0
|
|
674
683
|
[2.0.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.0.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grok-telegram-bot",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Control the official Grok Build CLI from Telegram over the Agent Client Protocol (ACP). Sign in with your xAI account, switch projects, resume sessions, stream responses with diffs, queue follow-ups, manage multiple sign-ins, and run 24/7 as a cross-platform background service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/scripts/_push-branch.mjs
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
|
|
4
|
-
const files =
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"src/bot/bot.ts",
|
|
11
|
-
"src/bot/handlers/accounts.ts",
|
|
12
|
-
"src/bot/permission-service.ts",
|
|
13
|
-
"src/bot/reauth-controller.ts",
|
|
14
|
-
"src/config.ts",
|
|
15
|
-
"src/grok/client.ts",
|
|
16
|
-
"test/auth-and-permissions.test.ts",
|
|
17
|
-
];
|
|
18
|
-
|
|
4
|
+
const files = process.argv.slice(2);
|
|
5
|
+
if (files.length < 3) {
|
|
6
|
+
console.error("Usage: node scripts/_push-branch.mjs <branch> <message> <file>...");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const [branch, message, ...paths] = files;
|
|
19
10
|
const owner = "artickc";
|
|
20
11
|
const repo = "grok-telegram-bot";
|
|
21
|
-
const branch = "feat/fix-rotation-and-auto-approve";
|
|
22
|
-
const message =
|
|
23
|
-
"fix: account rotation swaps auth.json headlessly; auto-approve session permissions";
|
|
24
12
|
|
|
25
13
|
// eslint-disable-next-line no-control-regex
|
|
26
14
|
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
@@ -43,29 +31,35 @@ function ghApi(method, path, body) {
|
|
|
43
31
|
}
|
|
44
32
|
const text = (r.stdout || "").replace(ANSI_RE, "").trim();
|
|
45
33
|
if (!text) return {};
|
|
46
|
-
|
|
47
|
-
return JSON.parse(text);
|
|
48
|
-
} catch (e) {
|
|
49
|
-
throw new Error(`JSON parse failed for ${method} ${path}: ${text.slice(0, 200)}`);
|
|
50
|
-
}
|
|
34
|
+
return JSON.parse(text);
|
|
51
35
|
}
|
|
52
36
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
37
|
+
// Ensure branch exists from main if missing.
|
|
38
|
+
try {
|
|
39
|
+
ghApi("GET", `repos/${owner}/${repo}/git/ref/heads/${branch}`);
|
|
40
|
+
} catch {
|
|
41
|
+
const main = ghApi("GET", `repos/${owner}/${repo}/git/ref/heads/main`);
|
|
42
|
+
ghApi("POST", `repos/${owner}/${repo}/git/refs`, {
|
|
43
|
+
ref: `refs/heads/${branch}`,
|
|
44
|
+
sha: main.object.sha,
|
|
45
|
+
});
|
|
46
|
+
console.log("created branch", branch, "from main");
|
|
47
|
+
}
|
|
57
48
|
|
|
49
|
+
const ref = ghApi("GET", `repos/${owner}/${repo}/git/ref/heads/${branch}`);
|
|
50
|
+
const baseSha = ref.object.sha;
|
|
58
51
|
const baseCommit = ghApi("GET", `repos/${owner}/${repo}/git/commits/${baseSha}`);
|
|
59
52
|
const baseTree = baseCommit.tree.sha;
|
|
53
|
+
console.log("base", baseSha);
|
|
60
54
|
|
|
61
55
|
const tree = [];
|
|
62
|
-
for (const path of
|
|
56
|
+
for (const path of paths) {
|
|
63
57
|
const content = readFileSync(path, "utf8");
|
|
64
58
|
const blob = ghApi("POST", `repos/${owner}/${repo}/git/blobs`, {
|
|
65
59
|
content: Buffer.from(content, "utf8").toString("base64"),
|
|
66
60
|
encoding: "base64",
|
|
67
61
|
});
|
|
68
|
-
tree.push({ path, mode: "100644", type: "blob", sha: blob.sha });
|
|
62
|
+
tree.push({ path: path.replace(/\\/g, "/"), mode: "100644", type: "blob", sha: blob.sha });
|
|
69
63
|
console.log("blob", path, blob.sha.slice(0, 8));
|
|
70
64
|
}
|
|
71
65
|
|
|
@@ -73,15 +67,11 @@ const newTree = ghApi("POST", `repos/${owner}/${repo}/git/trees`, {
|
|
|
73
67
|
base_tree: baseTree,
|
|
74
68
|
tree,
|
|
75
69
|
});
|
|
76
|
-
console.log("tree", newTree.sha);
|
|
77
|
-
|
|
78
70
|
const commit = ghApi("POST", `repos/${owner}/${repo}/git/commits`, {
|
|
79
71
|
message,
|
|
80
72
|
tree: newTree.sha,
|
|
81
73
|
parents: [baseSha],
|
|
82
74
|
});
|
|
83
|
-
console.log("commit", commit.sha);
|
|
84
|
-
|
|
85
75
|
ghApi("PATCH", `repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
86
76
|
sha: commit.sha,
|
|
87
77
|
force: false,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const owner = "artickc";
|
|
4
|
+
const repo = "grok-telegram-bot";
|
|
5
|
+
const version = process.argv[2] || "2.2.1";
|
|
6
|
+
const tag = `v${version}`;
|
|
7
|
+
const message = process.argv[3] || `${tag} — pin permission prompts until resolved`;
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line no-control-regex
|
|
10
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
11
|
+
|
|
12
|
+
function ghApi(method, path, body) {
|
|
13
|
+
const args = ["api", "-X", method, path];
|
|
14
|
+
const opts = {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
17
|
+
shell: false,
|
|
18
|
+
env: { ...process.env, NO_COLOR: "1", CLICOLOR: "0", FORCE_COLOR: "0", GH_FORCE_TTY: "0" },
|
|
19
|
+
};
|
|
20
|
+
if (body !== undefined) {
|
|
21
|
+
args.push("--input", "-");
|
|
22
|
+
opts.input = JSON.stringify(body);
|
|
23
|
+
}
|
|
24
|
+
const r = spawnSync("gh", args, opts);
|
|
25
|
+
if (r.status !== 0) {
|
|
26
|
+
throw new Error(`gh api ${method} ${path} failed (${r.status}): ${r.stderr || r.stdout}`);
|
|
27
|
+
}
|
|
28
|
+
const text = (r.stdout || "").replace(ANSI_RE, "").trim();
|
|
29
|
+
if (!text) return {};
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ref = ghApi("GET", `repos/${owner}/${repo}/git/ref/heads/main`);
|
|
34
|
+
const sha = ref.object.sha;
|
|
35
|
+
console.log("main", sha);
|
|
36
|
+
|
|
37
|
+
const tagObj = ghApi("POST", `repos/${owner}/${repo}/git/tags`, {
|
|
38
|
+
tag,
|
|
39
|
+
message,
|
|
40
|
+
object: sha,
|
|
41
|
+
type: "commit",
|
|
42
|
+
tagger: {
|
|
43
|
+
name: "artickc",
|
|
44
|
+
email: "artickc@users.noreply.github.com",
|
|
45
|
+
date: new Date().toISOString(),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
ghApi("POST", `repos/${owner}/${repo}/git/refs`, {
|
|
49
|
+
ref: `refs/tags/${tag}`,
|
|
50
|
+
sha: tagObj.sha,
|
|
51
|
+
});
|
|
52
|
+
console.log("created tag", tag, "->", sha);
|
package/src/bot/bot.ts
CHANGED
|
@@ -125,8 +125,12 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
125
125
|
|
|
126
126
|
// Permission handling: default is auto-approve (prefer "this session" / always).
|
|
127
127
|
// Interactive Approve/Deny buttons only when both trust-all and auto-approve are off.
|
|
128
|
+
// Interactive prompts are pinned so they aren't lost in a busy chat; on
|
|
129
|
+
// settle we re-pin the status panel (private chats keep a single pin).
|
|
128
130
|
const autoApprovePerms = cfg.autoApprovePermissions || cfg.trustAllTools;
|
|
129
|
-
const permissions = new PermissionService(bot.api, registry, autoApprovePerms
|
|
131
|
+
const permissions = new PermissionService(bot.api, registry, autoApprovePerms, {
|
|
132
|
+
onUnpinned: (chatId) => statusPanel.ensurePinned(chatId),
|
|
133
|
+
});
|
|
130
134
|
acp.permissionHandler = (p) => permissions.handle(p);
|
|
131
135
|
|
|
132
136
|
// The bot pins/unpins the status panel, and Telegram emits a "pinned a
|
|
@@ -147,9 +151,14 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
147
151
|
});
|
|
148
152
|
|
|
149
153
|
bot.callbackQuery(/^perm:(\d+):(\d+)$/, async (ctx) => {
|
|
154
|
+
// resolveChoice unpins the prompt; we then rewrite it to the chosen label.
|
|
150
155
|
const label = permissions.resolveChoice(ctx.match![1]!, Number(ctx.match![2]));
|
|
151
156
|
await ctx.answerCallbackQuery({ text: label ?? "Expired" });
|
|
152
|
-
await ctx
|
|
157
|
+
await ctx
|
|
158
|
+
.editMessageText(label ? `\u{1F510} ${label}` : "\u{1F510} (expired)", {
|
|
159
|
+
reply_markup: { inline_keyboard: [] },
|
|
160
|
+
})
|
|
161
|
+
.catch(() => {});
|
|
153
162
|
});
|
|
154
163
|
|
|
155
164
|
bot.callbackQuery(/^permsw:(\d+)$/, async (ctx) => {
|
|
@@ -153,6 +153,20 @@ export class StatusPanel {
|
|
|
153
153
|
log.debug("status create/pin failed:", (err as Error).message);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Re-pin the existing status panel (if any). Used after a temporary pin
|
|
159
|
+
* (e.g. a permission prompt) is unpinned so the status panel stays visible.
|
|
160
|
+
*/
|
|
161
|
+
async ensurePinned(chatId: number): Promise<void> {
|
|
162
|
+
const id = this.settings.get(chatId).statusMessageId;
|
|
163
|
+
if (!id) return;
|
|
164
|
+
try {
|
|
165
|
+
await this.api.pinChatMessage(chatId, id, { disable_notification: true });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
log.debug("status re-pin failed:", (err as Error).message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
156
170
|
}
|
|
157
171
|
|
|
158
172
|
function isNotModified(err: unknown): boolean {
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* PermissionService — turns Grok's ACP `session/request_permission` into either
|
|
3
3
|
* an automatic session-level approval (default) or inline Approve/Deny buttons.
|
|
4
4
|
*
|
|
5
|
+
* Interactive prompts are **pinned** so they stay visible while the chat is
|
|
6
|
+
* busy streaming other messages, then **unpinned** on approve / deny / timeout.
|
|
7
|
+
*
|
|
5
8
|
* Auto-approve prefers "allow for this session" / "always allow" options so the
|
|
6
9
|
* agent stops re-prompting mid-turn. Interactive mode is only used when
|
|
7
10
|
* auto-approve is off (GROK_TRUST_ALL_TOOLS=false and AUTO_APPROVE_PERMISSIONS=false).
|
|
@@ -31,6 +34,16 @@ interface Pending {
|
|
|
31
34
|
sessionId: string;
|
|
32
35
|
messageId?: number;
|
|
33
36
|
timer: NodeJS.Timeout;
|
|
37
|
+
/** True once the message has been pinned (so we know to unpin on settle). */
|
|
38
|
+
pinned: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PermissionServiceOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Called after a permission prompt is unpinned (approve / deny / timeout).
|
|
44
|
+
* Use to re-pin the status panel if Telegram only keeps one pin per chat.
|
|
45
|
+
*/
|
|
46
|
+
onUnpinned?: (chatId: number) => void | Promise<void>;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export class PermissionService {
|
|
@@ -38,13 +51,16 @@ export class PermissionService {
|
|
|
38
51
|
private seq = 0;
|
|
39
52
|
/** When true, every permission request is auto-approved (session-scope preferred). */
|
|
40
53
|
autoApprove: boolean;
|
|
54
|
+
private readonly onUnpinned?: (chatId: number) => void | Promise<void>;
|
|
41
55
|
|
|
42
56
|
constructor(
|
|
43
57
|
private readonly api: Api,
|
|
44
58
|
private readonly registry: RuntimeRegistry,
|
|
45
59
|
autoApprove = true,
|
|
60
|
+
opts?: PermissionServiceOptions,
|
|
46
61
|
) {
|
|
47
62
|
this.autoApprove = autoApprove;
|
|
63
|
+
this.onUnpinned = opts?.onUnpinned;
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
/** Handle a permission request: auto-approve (default), ask the chat, or allow if unattended. */
|
|
@@ -78,6 +94,7 @@ export class PermissionService {
|
|
|
78
94
|
if (canSwitch) kb.text(`\u{1F500} Switch to ${label}`, `permsw:${reqId}`);
|
|
79
95
|
|
|
80
96
|
let messageId: number | undefined;
|
|
97
|
+
let pinned = false;
|
|
81
98
|
try {
|
|
82
99
|
const msg = await this.api.sendMessage(
|
|
83
100
|
chatId,
|
|
@@ -88,6 +105,14 @@ export class PermissionService {
|
|
|
88
105
|
},
|
|
89
106
|
);
|
|
90
107
|
messageId = msg.message_id;
|
|
108
|
+
// Pin so the prompt stays visible while the chat streams other messages.
|
|
109
|
+
// disable_notification:true — the send already notified; no second ping.
|
|
110
|
+
try {
|
|
111
|
+
await this.api.pinChatMessage(chatId, messageId, { disable_notification: true });
|
|
112
|
+
pinned = true;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
log.warn("failed to pin permission prompt:", (e as Error).message);
|
|
115
|
+
}
|
|
91
116
|
} catch (e) {
|
|
92
117
|
log.warn("failed to send permission prompt:", (e as Error).message);
|
|
93
118
|
return autoDecideSession(params);
|
|
@@ -95,11 +120,21 @@ export class PermissionService {
|
|
|
95
120
|
|
|
96
121
|
return new Promise<PermissionOutcome>((resolve) => {
|
|
97
122
|
const timer = setTimeout(() => {
|
|
123
|
+
const p = this.pending.get(reqId);
|
|
124
|
+
if (!p) return;
|
|
98
125
|
this.pending.delete(reqId);
|
|
99
|
-
void this.
|
|
126
|
+
void this.finishPrompt(p, "\u231B Approval timed out \u2014 denied.");
|
|
100
127
|
resolve({ outcome: { outcome: "cancelled" } });
|
|
101
128
|
}, TIMEOUT_MS);
|
|
102
|
-
this.pending.set(reqId, {
|
|
129
|
+
this.pending.set(reqId, {
|
|
130
|
+
resolve,
|
|
131
|
+
options: params.options,
|
|
132
|
+
chatId,
|
|
133
|
+
sessionId: params.sessionId,
|
|
134
|
+
messageId,
|
|
135
|
+
timer,
|
|
136
|
+
pinned,
|
|
137
|
+
});
|
|
103
138
|
});
|
|
104
139
|
}
|
|
105
140
|
|
|
@@ -111,9 +146,12 @@ export class PermissionService {
|
|
|
111
146
|
this.pending.delete(reqId);
|
|
112
147
|
const opt = p.options[index];
|
|
113
148
|
if (!opt) {
|
|
149
|
+
void this.finishPrompt(p, "\u{1F510} (cancelled)");
|
|
114
150
|
p.resolve({ outcome: { outcome: "cancelled" } });
|
|
115
151
|
return undefined;
|
|
116
152
|
}
|
|
153
|
+
// Unpin + mark resolved (caller typically edits the message text too).
|
|
154
|
+
void this.unpinOnly(p);
|
|
117
155
|
p.resolve({ outcome: { outcome: "selected", optionId: opt.optionId } });
|
|
118
156
|
return opt.name;
|
|
119
157
|
}
|
|
@@ -122,6 +160,31 @@ export class PermissionService {
|
|
|
122
160
|
sessionFor(reqId: string): string | undefined {
|
|
123
161
|
return this.pending.get(reqId)?.sessionId;
|
|
124
162
|
}
|
|
163
|
+
|
|
164
|
+
/** Unpin (if pinned) and optionally rewrite the prompt message. */
|
|
165
|
+
private async finishPrompt(p: Pending, text?: string): Promise<void> {
|
|
166
|
+
if (p.messageId !== undefined && text) {
|
|
167
|
+
await this.api.editMessageText(p.chatId, p.messageId, text, { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
168
|
+
}
|
|
169
|
+
await this.unpinOnly(p);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Unpin the permission prompt and restore the status pin if any. */
|
|
173
|
+
private async unpinOnly(p: Pending): Promise<void> {
|
|
174
|
+
if (p.pinned && p.messageId !== undefined) {
|
|
175
|
+
p.pinned = false;
|
|
176
|
+
await this.api.unpinChatMessage(p.chatId, p.messageId).catch((e) => {
|
|
177
|
+
log.debug("unpin permission prompt failed:", (e as Error).message);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (this.onUnpinned) {
|
|
181
|
+
try {
|
|
182
|
+
await this.onUnpinned(p.chatId);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
log.debug("onUnpinned hook failed:", (e as Error).message);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
125
188
|
}
|
|
126
189
|
|
|
127
190
|
function describe(
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bump package.json on main + create vX.Y.Z tag via the GitHub API
|
|
3
|
-
* (local git push/commit is restricted in this environment).
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
|
|
8
|
-
const owner = "artickc";
|
|
9
|
-
const repo = "grok-telegram-bot";
|
|
10
|
-
const branch = "main";
|
|
11
|
-
const version = "2.2.0";
|
|
12
|
-
const tag = `v${version}`;
|
|
13
|
-
const message = `v${version} — headless account rotation + auto-approve session permissions`;
|
|
14
|
-
|
|
15
|
-
// eslint-disable-next-line no-control-regex
|
|
16
|
-
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
17
|
-
|
|
18
|
-
function ghApi(method, path, body) {
|
|
19
|
-
const args = ["api", "-X", method, path];
|
|
20
|
-
const opts = {
|
|
21
|
-
encoding: "utf8",
|
|
22
|
-
maxBuffer: 50 * 1024 * 1024,
|
|
23
|
-
shell: false,
|
|
24
|
-
env: { ...process.env, NO_COLOR: "1", CLICOLOR: "0", FORCE_COLOR: "0", GH_FORCE_TTY: "0" },
|
|
25
|
-
};
|
|
26
|
-
if (body !== undefined) {
|
|
27
|
-
args.push("--input", "-");
|
|
28
|
-
opts.input = JSON.stringify(body);
|
|
29
|
-
}
|
|
30
|
-
const r = spawnSync("gh", args, opts);
|
|
31
|
-
if (r.status !== 0) {
|
|
32
|
-
throw new Error(`gh api ${method} ${path} failed (${r.status}): ${r.stderr || r.stdout}`);
|
|
33
|
-
}
|
|
34
|
-
const text = (r.stdout || "").replace(ANSI_RE, "").trim();
|
|
35
|
-
if (!text) return {};
|
|
36
|
-
return JSON.parse(text);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const ref = ghApi("GET", `repos/${owner}/${repo}/git/ref/heads/${branch}`);
|
|
40
|
-
const baseSha = ref.object.sha;
|
|
41
|
-
const baseCommit = ghApi("GET", `repos/${owner}/${repo}/git/commits/${baseSha}`);
|
|
42
|
-
const baseTree = baseCommit.tree.sha;
|
|
43
|
-
console.log("base", baseSha);
|
|
44
|
-
|
|
45
|
-
const content = readFileSync("package.json", "utf8");
|
|
46
|
-
const blob = ghApi("POST", `repos/${owner}/${repo}/git/blobs`, {
|
|
47
|
-
content: Buffer.from(content, "utf8").toString("base64"),
|
|
48
|
-
encoding: "base64",
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const newTree = ghApi("POST", `repos/${owner}/${repo}/git/trees`, {
|
|
52
|
-
base_tree: baseTree,
|
|
53
|
-
tree: [{ path: "package.json", mode: "100644", type: "blob", sha: blob.sha }],
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const commit = ghApi("POST", `repos/${owner}/${repo}/git/commits`, {
|
|
57
|
-
message,
|
|
58
|
-
tree: newTree.sha,
|
|
59
|
-
parents: [baseSha],
|
|
60
|
-
});
|
|
61
|
-
console.log("commit", commit.sha);
|
|
62
|
-
|
|
63
|
-
ghApi("PATCH", `repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
64
|
-
sha: commit.sha,
|
|
65
|
-
force: false,
|
|
66
|
-
});
|
|
67
|
-
console.log("updated", branch, "->", commit.sha);
|
|
68
|
-
|
|
69
|
-
// Annotated tag for release workflow.
|
|
70
|
-
const tagObj = ghApi("POST", `repos/${owner}/${repo}/git/tags`, {
|
|
71
|
-
tag,
|
|
72
|
-
message: message,
|
|
73
|
-
object: commit.sha,
|
|
74
|
-
type: "commit",
|
|
75
|
-
tagger: {
|
|
76
|
-
name: "artickc",
|
|
77
|
-
email: "artickc@users.noreply.github.com",
|
|
78
|
-
date: new Date().toISOString(),
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
console.log("tag object", tagObj.sha);
|
|
82
|
-
|
|
83
|
-
ghApi("POST", `repos/${owner}/${repo}/git/refs`, {
|
|
84
|
-
ref: `refs/tags/${tag}`,
|
|
85
|
-
sha: tagObj.sha,
|
|
86
|
-
});
|
|
87
|
-
console.log("created tag", tag);
|