Skip to content

Commit c57b06d

Browse files
amtiYoclaude
andcommitted
merge: v1.2.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FxNv1rk7jMQtb9tMBREc5B
2 parents 8d265a1 + a426e2c commit c57b06d

17 files changed

Lines changed: 957 additions & 169 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ jobs:
1818
name: Build, vet, test
1919
runs-on: ubuntu-latest
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2222

2323
- name: Set up Go
24-
uses: actions/setup-go@v5
24+
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
2525
with:
2626
go-version-file: go.mod
2727
cache: true
@@ -45,10 +45,10 @@ jobs:
4545
name: Vulnerability scan
4646
runs-on: ubuntu-latest
4747
steps:
48-
- uses: actions/checkout@v4
48+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
4949

5050
- name: Set up Go
51-
uses: actions/setup-go@v5
51+
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
5252
with:
5353
go-version-file: go.mod
5454
cache: true
@@ -62,7 +62,7 @@ jobs:
6262
name: Docker build
6363
runs-on: ubuntu-latest
6464
steps:
65-
- uses: actions/checkout@v4
65+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
6666

6767
- name: Build image
6868
run: docker build -t branchy:ci .

‎.github/workflows/release.yml‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,26 @@ jobs:
1414
name: Build image and publish release
1515
runs-on: ubuntu-latest
1616
steps:
17-
- uses: actions/checkout@v4
17+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
18+
with:
19+
fetch-depth: 0
20+
21+
- name: Require the tag on the branch its channel is published from
22+
# docs/versioning.md: pre-releases are tagged on dev, stable releases on
23+
# main. A stable tag that never went through main would publish
24+
# something the production branch has not seen.
25+
run: |
26+
if printf '%s' "${GITHUB_REF_NAME}" | grep -Eq -- '-(alpha|beta|rc)\.'; then
27+
branch=dev
28+
else
29+
branch=main
30+
fi
31+
git fetch --no-tags origin "${branch}:refs/remotes/origin/${branch}"
32+
if ! git merge-base --is-ancestor "${GITHUB_SHA}" "origin/${branch}"; then
33+
echo "tag ${GITHUB_REF_NAME} is not an ancestor of origin/${branch}"
34+
exit 1
35+
fi
36+
1837
1938
- name: Resolve build metadata
2039
id: meta
@@ -47,22 +66,25 @@ jobs:
4766
capture { print }
4867
' CHANGELOG.md >"${notes_file}"
4968
if [ ! -s "${notes_file}" ]; then
50-
printf 'Release %s\n' "${version}" >"${notes_file}"
69+
# A release whose notes are the word "Release" tells nobody anything,
70+
# and the changelog section is the only place they are written.
71+
echo "CHANGELOG.md has no '## ${version}' section"
72+
exit 1
5173
fi
5274
echo "file=${notes_file}" >>"$GITHUB_OUTPUT"
5375
5476
- name: Log in to GitHub Container Registry
55-
uses: docker/login-action@v3
77+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
5678
with:
5779
registry: ghcr.io
5880
username: ${{ github.actor }}
5981
password: ${{ secrets.GITHUB_TOKEN }}
6082

6183
- name: Set up Buildx
62-
uses: docker/setup-buildx-action@v3
84+
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
6385

6486
- name: Build and push image
65-
uses: docker/build-push-action@v6
87+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
6688
with:
6789
context: .
6890
push: true

‎AGENTS.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ Temporary Telegram/GitHub failures should retry with `retry_at` and `attempts`.
8383

8484
## Versioning
8585

86+
- Work on `dev`. Pre-releases (`-alpha.N`, `-beta.N`, `-rc.N`) are tagged on
87+
`dev`; stable versions are tagged on `main`, on the merge commit from `dev`.
88+
The test bot runs `dev`, the production bot runs `main`.
8689
- Follow `docs/versioning.md` for release tags.
8790
- Keep the first release line as `v0.1.0-alpha.1`, `v0.1.0-beta.1`, `v0.1.0-rc.1`, then `v0.1.0`.
8891
- Use patch versions for fixes, minor versions for MVP-compatible product or operations improvements, and reserve `v1.0.0` for a stable production contract.
@@ -143,3 +146,17 @@ Branchy is licensed under Apache-2.0. Preserve the root `LICENSE` and `NOTICE` f
143146
`editEphemeralMessageText`. Notifications are rich messages and group
144147
overlays are ephemeral ones, so a server without them makes Branchy mute.
145148

149+
## Deploying
150+
151+
Do not invent a deploy. [`docs/releases.md`](docs/releases.md) has a **Deploying**
152+
section describing this stack exactly: which host directory it lives in, which
153+
env file names the image, which networks it needs, and how to roll back. Read it
154+
before touching anything on the host.
155+
156+
Two rules that hold everywhere and are easy to get wrong:
157+
158+
- **Nothing is built on the host.** A production stack pulls the image the
159+
release workflow published. A `build:` section in a production manifest is a
160+
bug.
161+
- **Pin the digest, not the tag.** A tag moves; a digest names one build that was
162+
tested, and a rollback becomes one line with nothing to rebuild.

‎CHANGELOG.md‎

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,103 @@
22

33
All notable Branchy changes are documented here.
44

5-
Branchy uses SemVer-style versions with pre-release tags before `v1.0.0`. Release
6-
notes should be copied from the relevant changelog section and lightly edited for
7-
GitHub Releases.
5+
The `## <tag>` section of this file *is* the GitHub Release body: the release
6+
workflow copies it verbatim and refuses a tag that has no section. Write it
7+
for whoever has to decide whether to upgrade.
8+
9+
See [`docs/versioning.md`](docs/versioning.md) for what the numbers mean and
10+
[`docs/releases.md`](docs/releases.md) for how a release is published.
811

912
## Unreleased
1013

1114
Use this section for changes that are merged but not released yet.
1215

16+
## v1.2.1 - 2026-09-09
17+
18+
An About card, a group panel that can be closed, and the fix for a hole that let
19+
any group member delete Branchy's notification cards.
20+
21+
22+
### Added
23+
24+
- `docs/releases.md` gained a **Deploying** section, and `AGENTS.md` points at it.
25+
Releasing was documented; deploying was not, in any repository in the family —
26+
the process stopped at "deploy it" and never said how. That gap mattered more
27+
after the stacks moved from building on the host to pulling a published image,
28+
because the procedure changed on the same day. The section names this stack's
29+
host directory, its env file, the variable that selects the image, the networks
30+
it needs, and what a rollback actually is.
31+
32+
- An **About** card, reachable from the main menu and from the group panel. It
33+
states the running version — the same string `/healthz` reports — plus the
34+
supported events, the repository, the license, and the admin contact. Until
35+
now nothing in the interface could answer "which version are you running",
36+
which is the first question any complaint has to answer. The repository is a
37+
link in the text rather than a second button, because two ways to reach one
38+
place is duplication.
39+
- **Close** on the group panel, and on the About card when it is opened from a
40+
group. Branchy had no way to take a panel back out of a group chat. Close acts
41+
only on ephemeral messages: Telegram lets a client send any callback data for
42+
any message it can see, so honouring `close` against a public message would
43+
have let anyone in a group delete Branchy's notification cards. There is no
44+
Close in DM, where the conversation is the panel and there is nothing to
45+
close.
46+
- `deploy/ws04/compose.yaml`, the production stack, pulling the image the
47+
release workflow publishes to GHCR. The stack on the host built its own image
48+
from a working copy, so what served users was not the artifact CI had tested,
49+
scanned and published, and nothing on the host could say which commit it came
50+
from. `BRANCHY_IMAGE` has no default: an unset one stops the stack instead of
51+
quietly starting something else.
52+
53+
### Changed
54+
55+
- One versioning and release document for the whole family. `docs/versioning.md`
56+
and `docs/releases.md` are now byte-identical across every Asterfield
57+
repository apart from two clearly marked sections: this repository's own
58+
version line, and the surface where a change here breaks something. They spell
59+
out what each of the three numbers means, what the `-alpha.N` suffix counts,
60+
when alpha becomes beta and when it is legitimate to skip to rc or run a
61+
pre-release in production.
62+
- **Pre-releases are now tagged on `dev`, not `main`.** Only stable versions are
63+
tagged on `main`, on the merge commit from `dev`. `release.yml` had no branch
64+
check at all before, so a tag pushed from any branch would publish; it now
65+
refuses a tag that is not on the branch its channel is published from.
66+
Earlier pre-releases were tagged on `main` under the previous rule; they are
67+
left as they are.
68+
69+
70+
- `github.com/FreshLabDev/tg` moves to `v0.0.1-alpha.7`. It carries one fix:
71+
a preflight probe is marked as a probe, so the `404 method not found` it
72+
expects stops being counted and logged as a transport failure. All four bots
73+
on the shared client now run the same version.
74+
- The same action is now called the same thing on every screen. Three sibling
75+
settings screens all returned to the settings hub and each named it
76+
differently: the draft branch-filter screen said `Done`, the draft release
77+
screen said `Back`, and the draft pull-request screen flipped between the two
78+
depending on how many actions were selected. All three now say `Back`, which
79+
is what they do — nothing on them is committed, and the hub is the screen the
80+
user came from. `Done` survives in exactly one place, the draft branch list,
81+
where it skips a level rather than stepping back. `Save branches` on the edit
82+
branch list is now `Save`, matching the other two edit screens that commit.
83+
- Single-select options are marked `◉` and `◎` instead of `●` and `○`, the pair
84+
the rest of the bot family uses. Multi-select options keep `■`/`□`, so the two
85+
kinds of list still cannot be confused for each other.
86+
- Callbacks arriving from a group are answered on the ephemeral panel itself
87+
(`editEphemeralMessageText`) rather than through `editMessageText`, which
88+
cannot address an ephemeral message. Before this, a button on the group panel
89+
would have left the panel frozen and answered in DM. A `home` callback from a
90+
group now returns the group panel instead of building the DM menu, which
91+
would have put one person's GitHub login and subscription count into a chat
92+
they share with everyone else. A public group message is never edited into a
93+
panel at all: every panel Branchy shows in a group is ephemeral, so a public
94+
one is a delivered notification card, and callback data can be sent for any
95+
visible message. Such a callback is answered in DM.
96+
- Both Telegram command descriptions moved out of `cmd/branchy/main.go` into
97+
`internal/bot/copy.go`, next to the rest of Branchy's fixed copy. Behaviour is
98+
unchanged — one list per scope, still `all_private_chats` plus an ephemeral
99+
`all_group_chats`. The file records why there is no list per `language_code`:
100+
the English-only interface is a product boundary, not missing work.
101+
13102
## v1.2.1-alpha.1 - 2026-09-08
14103

15104
Internal, with one visible consequence: Branchy will not start against a
@@ -781,7 +870,7 @@ Telegram-first setup flow are unchanged.
781870
- Subscription view, pause, edit, delete, and test notification actions.
782871
- Startup SQL migrations tracked in `schema_migrations`.
783872
- `/healthz` runtime health endpoint.
784-
- Apache-2.0 license under FreshLab.
873+
- Apache-2.0 license under Asterfield.
785874
- Project documentation for architecture, GitHub integration, Telegram behavior,
786875
versioning, and release process.
787876
- Pagination for the repository and branch pickers (Prev/Next) instead of

‎LICENSE‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ recommend that a file or class name and description of purpose be included on
169169
the same "printed page" as the copyright notice for easier identification within
170170
third-party archives.
171171

172-
Copyright 2026 FreshLab
172+
Copyright 2026 Asterfield
173173

174174
Licensed under the Apache License, Version 2.0 (the "License");
175175
you may not use this file except in compliance with the License.

‎NOTICE‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
Branchy
2-
Copyright 2026 FreshLab
2+
Copyright 2026 Asterfield
33

44
This product is licensed under the Apache License, Version 2.0.

‎README.md‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,18 @@ All user setup is button-driven inside Telegram.
138138
5. Choose DM delivery or an eligible Telegram group.
139139
6. View, pause, edit, delete, or test subscriptions from the inline menus.
140140

141+
`About` on the main menu reports the running version — the same string
142+
`/healthz` returns — along with the supported events, the repository, and who
143+
to reach.
144+
141145
Groups become available only after Branchy has seen the group. Before group
142146
delivery is enabled, Branchy verifies that the Telegram user is a group
143147
`creator` or `administrator`.
144148

145149
In groups, `/start` is registered as a Bot API 10.2 ephemeral command. Its DM
146150
prompt is visible only to the user who invoked it; Branchy never posts a public
147-
fallback into the group.
151+
fallback into the group. That panel offers `About` and `Close`; `Close` removes
152+
it from the chat.
148153

149154
---
150155

@@ -264,6 +269,22 @@ only; restrict them at your reverse proxy if you do not want them public.
264269

265270
---
266271

272+
## Production
273+
274+
`deploy/ws04/compose.yaml` is the production stack. It pulls the image the
275+
release workflow published to GHCR and never builds one:
276+
277+
```sh
278+
# in the stack directory, alongside .env
279+
BRANCHY_IMAGE=ghcr.io/freshlabdev/branchy@sha256:<digest> docker compose up -d
280+
```
281+
282+
Pin `BRANCHY_IMAGE` by digest rather than tag. A digest names one exact build,
283+
so a rollback is a one-line change with nothing to rebuild, and `docker
284+
inspect` on the running container answers which commit it came from.
285+
286+
---
287+
267288
## Testing
268289

269290
```sh
@@ -294,6 +315,6 @@ docker compose config
294315
</p>
295316

296317
<p align="center">
297-
Branchy is open source software by FreshLab.<br/>
298-
Copyright 2026 FreshLab.
318+
Branchy is open source software by Asterfield.<br/>
319+
Copyright 2026 Asterfield.
299320
</p>

‎cmd/branchy/main.go‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func run() error {
8989
PublicBaseURL: cfg.PublicBaseURL,
9090
GitHubWebhookSecret: cfg.GitHubWebhookSecret,
9191
}, store, gh, sealer)
92-
service := bot.NewBot(store, client, oauthSvc, gh, sealer, subSvc)
92+
service := bot.NewBot(store, client, oauthSvc, gh, sealer, subSvc, version)
9393
notificationWorker := outbox.NewWorker(store, client, outbox.Config{
9494
BatchSize: cfg.OutboxBatchSize,
9595
PollInterval: cfg.OutboxPollInterval,
@@ -206,6 +206,9 @@ type commandRegistrar interface {
206206
// ensureTelegramCommands runs independently from HTTP serving and polling.
207207
// Each scope is retried until it succeeds, so a transient first-deploy failure
208208
// cannot leave group /start public or unavailable until the next restart.
209+
//
210+
// The two lists come from internal/bot, where Branchy's fixed copy lives; that
211+
// file also records why there is one list per scope and not one per language.
209212
func ensureTelegramCommands(ctx context.Context, registrar commandRegistrar, retryBase time.Duration) {
210213
if retryBase <= 0 {
211214
retryBase = 30 * time.Second
@@ -218,9 +221,8 @@ func ensureTelegramCommands(ctx context.Context, registrar commandRegistrar, ret
218221
return
219222
}
220223
if !privateReady {
221-
commands := []tg.BotCommand{{Command: "start", Description: "Open the Branchy menu"}}
222224
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
223-
err := registrar.SetMyCommandsForScope(attemptCtx, commands, &tg.BotCommandScope{Type: "all_private_chats"})
225+
err := registrar.SetMyCommandsForScope(attemptCtx, bot.PrivateCommands(), &tg.BotCommandScope{Type: "all_private_chats"})
224226
cancel()
225227
if err != nil {
226228
slog.Warn("set private telegram commands failed; will retry", "error", err, "retry_in", delay)
@@ -229,11 +231,8 @@ func ensureTelegramCommands(ctx context.Context, registrar commandRegistrar, ret
229231
}
230232
}
231233
if !groupReady {
232-
commands := []tg.BotCommand{{
233-
Command: "start", Description: "Open Branchy privately", IsEphemeral: true,
234-
}}
235234
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
236-
err := registrar.SetMyCommandsForScope(attemptCtx, commands, &tg.BotCommandScope{Type: "all_group_chats"})
235+
err := registrar.SetMyCommandsForScope(attemptCtx, bot.GroupCommands(), &tg.BotCommandScope{Type: "all_group_chats"})
237236
cancel()
238237
if err != nil {
239238
slog.Warn("set group telegram commands failed; will retry", "error", err, "retry_in", delay)

0 commit comments

Comments
 (0)