Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
96a3eeb
feat(config): accept Bitbucket Cloud providers
AlexeyShalaev Oct 8, 2026
9836911
feat(bitbucket): read Bitbucket Cloud repositories, files and pull re…
AlexeyShalaev Oct 8, 2026
8c6320c
test(report): an unknown provider type is azure-devops now that bitbu…
AlexeyShalaev Oct 8, 2026
6605bf5
docs: say that Bitbucket Cloud is read only for now
AlexeyShalaev Oct 8, 2026
b247dab
feat(platform): add Caps.NoLabels for platforms without pull request …
AlexeyShalaev Oct 8, 2026
8740066
feat(marker): read and write the marker as a Markdown reference defin…
AlexeyShalaev Oct 8, 2026
756f89d
feat(distribute): respect Caps.NoLabels and Caps.Marker on Bitbucket
AlexeyShalaev Oct 8, 2026
15342d5
test(distribute): keep the Bitbucket push test's comment self-contained
AlexeyShalaev Oct 8, 2026
8e099da
feat(bitbucket): write to Bitbucket Cloud
AlexeyShalaev Oct 8, 2026
755c559
docs: describe delivery to Bitbucket Cloud
AlexeyShalaev Oct 8, 2026
4ecae10
feat(platform): add Caps.ClosedImmutable and Caps.BodyControls
AlexeyShalaev Oct 8, 2026
84a8bfc
feat(decide): remember declines without writes where closed pull requ…
AlexeyShalaev Oct 8, 2026
7df8af2
feat(prbody): offer no tick boxes where descriptions cannot carry them
AlexeyShalaev Oct 8, 2026
7270bf8
test(fake): add the Bitbucket flavor
AlexeyShalaev Oct 8, 2026
d4bea94
feat(distribute): deliver to platforms whose closed pull requests are…
AlexeyShalaev Oct 8, 2026
5bb1337
test(distribute): delivery scenarios on Bitbucket
AlexeyShalaev Oct 8, 2026
f6fd04e
docs: describe declines on Bitbucket Cloud
AlexeyShalaev Oct 8, 2026
b89943b
fix(marker): refuse a reference definition without its closing quote
AlexeyShalaev Oct 8, 2026
5e05d63
fix(distribute): record the opt-in state in a blocked open pull reque…
AlexeyShalaev Oct 8, 2026
0d092da
fix(prbody): write no HTML into descriptions on Bitbucket Cloud
AlexeyShalaev Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ no lock file, no header, no marker in their files. It never checks a target out
and never runs a script, hook, filter or LFS from the hub or a target. It delivers
nothing to a repository without the opt-in file, unless `targets.yml` subscribes it
(`opt_in: assumed`). There is no pull mode (targets do not
fetch from the hub), no remote pack sources, and no Bitbucket or Azure DevOps driver.
fetch from the hub), no remote pack sources, no Azure DevOps driver, and no hub on
Bitbucket Pipelines: Bitbucket Cloud is a provider a hub elsewhere delivers to.

## Mental model

Expand Down Expand Up @@ -283,7 +284,7 @@ covers everything under it). At most 64 KiB.

`.touchmark/operations.yml` (in the hub; `touchmark schema operations`):
`recreate[]` (`target`, `head`: acts while the branch head is that commit),
`forget_declines[]` (`target`, `pr`: acts once), `allow_mass_close` (`max`, `until`),
`forget_declines[]` (`target`, `pr`: acts once; on Bitbucket Cloud while present), `allow_mass_close` (`max`, `until`),
`adopt_unmarked` (`until`). Dates are `YYYY-MM-DD`, UTC, inclusive.

Environment, one set per provider; `<ID>` is the provider id upper-cased with `-` as
Expand Down Expand Up @@ -458,7 +459,7 @@ Each target ends with one outcome, and a reason for most:
| `opened`, `updated`, `unchanged`, `closed` | `updated`: `content`, `rebase`, `recreate`, `body`, `title`, `base-renamed`; `closed`: `no-diff`, `opted-out`, `target-dropped`, `duplicate` | nothing |
| `declined` | the declined pull request | nothing; see [memory](concepts/memory.md) to propose again |
| `skipped` | `not-opted-in`, `opted-out` (the opt-in file says `enabled: false`), `archived`, `disabled`, `empty`, `mirror`, `pending-deletion`, `prs-disabled`, `sha256`, `unsafe-opt-in`, `private-in-public-hub`, `superseded` | add the opt-in file (or `opt_in: assumed` in the hub), or nothing |
| `blocked` | `edited` (someone pushed to the sync branch), `branch-taken`, `branch-in-use`, `opt-in-invalid`, `marker-invalid`, `rules:<rule>`, `permission:<what>`, `cannot-sign`, `archived`, `mass-close` | tick *Rebuild this branch* or add a `recreate` entry; fix the opt-in file; grant the permission; add a signing key; `allow_mass_close` |
| `blocked` | `edited` (someone pushed to the sync branch), `branch-taken`, `branch-in-use`, `opt-in-invalid`, `marker-invalid`, `rules:<rule>`, `permission:<what>`, `cannot-sign`, `archived`, `mass-close` | tick *Rebuild this branch* (not on Bitbucket Cloud) or add a `recreate` entry; fix the opt-in file; grant the permission; add a signing key; `allow_mass_close` |
| `deferred` | `rate-limit`, `deadline`, `rollout-limit`, `provider-down`, `interrupted`, `cooldown` | nothing: the next run continues |
| `failed` | `transient`, `auth`, `access`, `git`, `integrity`, `race`, `secret-exposure`, `internal` | read the report's reason; `auth` and `access` are the credential |

Expand Down
3 changes: 2 additions & 1 deletion docs/concepts/delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ The body, in English:
`sensitive_paths` from `hub.yml`). Never cut;
3. a table of the changes, by file and pack: sensitive first, then deletions, updates,
additions, up to 100 rows;
4. a folded list of the files the repository made its own, with how to take them back;
4. a folded list of the files the repository made its own, with how to take them back
(a plain section on Bitbucket Cloud, which shows HTML as text);
5. blocks for the situation: *previously declined in #N*, *touchmark paused*, *the hub
proposes nothing more*;
6. the tick boxes;
Expand Down
46 changes: 45 additions & 1 deletion docs/concepts/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ current changes, or closes it as `no-diff`.

When touchmark first sees a decline, it records it in the marker, adds a tick box to the
description and leaves one comment saying what it remembered, when a new pull request
would come, and how to undo it. Two writes, once.
would come, and how to undo it. Two writes, once. (Not on Bitbucket Cloud, where a
declined pull request cannot be edited: see [below](#on-bitbucket-cloud).)

To undo a decline:

Expand All @@ -72,6 +73,42 @@ To refuse a file for good instead, add it to `ignore`, or set `enabled: false` i
file (deleting it works too, unless the hub subscribed the repository) to stop
receiving anything.

## On Bitbucket Cloud

A declined pull request on Bitbucket can never be edited or reopened, by anyone. touchmark
writes nothing to it, and remembers the decline from what the marker held while the pull
request was open:

- **The opt-in state is recorded up front.** Every pull request touchmark opens records
in its marker the state of the opt-in file it is proposed under, which the
acknowledgment records elsewhere. When the file changes while the pull request is
open, the next run records the new state, with one edit of the description even when
the proposed changes stay the same, and even when the target is blocked
(`branch-in-use`, `rules:*`, …): when the run writes nothing else to the pull
request, that edit changes only the marker. A push a branch restriction refuses at
run time is followed by that edit too.
- **A decline** holds while the opt-in file is parsed the same as that recorded state,
and lapses when the team edits `packs` or `ignore`, as an acknowledged decline does
elsewhere. A declined path becoming `local` or `ignored` lifts it too.
- **No comment, no tick box.** touchmark leaves no comment on the declined pull request
(with nothing to record that it did, it would post one every run), and descriptions
carry no tick boxes there: Bitbucket shows the HTML comments they rest on as text. The
report says `declined`, and the description's footnote names the way back.
- **Bringing declined content back** is a `forget_declines` entry, or a change of `packs`
or `ignore`. touchmark cannot mark the pull request revoked, so the entry acts on every
run while it is present: keep it until the pull request it brings is merged or closed.
Removed earlier, the decline is in force again; an open pull request stays (an open pull
request is stronger than memory), and its description then names the decline under
*Previously declined*.
- **A marker without the opt-in state** (one touchmark did not write in full) holds its
decline whatever the opt-in file says, since there is no state to compare with; the
report warns about it on every run and names the `forget_declines` entry that lifts
it.
- **Who closed it** counts as everywhere: Bitbucket names who declined a pull request.
touchmark's own closes write the close into the description in the same edit that
declines it; bots' closes wait for the cooldown, and the third in a row counts as a
decline.

## Limits

- Only the last 50 closed pull requests on the sync branches count.
Expand All @@ -83,3 +120,10 @@ receiving anything.
own: that decline is lost and the content is proposed again.
- Closed pull requests of the multi-gitter era, without a marker, are never declines:
their content is unknown.
- On Bitbucket Cloud the opt-in state of a decline is the one recorded by the last run
before the decline, whatever that run decided for the target. When the team changes
`packs` or `ignore` and declines the pull request before touchmark runs again, the
decline counts from the earlier state and has lapsed already: the content is proposed
once more, under the new choice. A blocked target does not widen that window: its run
records the state as well. Only a run that writes nothing to the target at all (a
failed or deferred target, `blocked:mass-close`) leaves it open until the next run.
11 changes: 10 additions & 1 deletion docs/guide/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ cleanup to stay safe:
| Entry | Acts | Then |
|---|---|---|
| `recreate` | while the sync branch's head is exactly `head` | a new push to the branch makes it inert |
| `forget_declines` | once: touchmark marks pull request `pr` revoked | inert |
| `forget_declines` | once: touchmark marks pull request `pr` revoked; on Bitbucket Cloud, while the entry is present (see below) | inert |
| `allow_mass_close` | up to `max` closes in a run, until the end of `until` (UTC) | inert after the date |
| `adopt_unmarked` | until the end of `until` (UTC) | inert after the date |

Expand All @@ -63,6 +63,15 @@ A target's team can do two of these themselves, from the pull request:
- tick **Propose this content again** in a declined pull request: the same as
`forget_declines`, or simply reopen it.

On Bitbucket Cloud descriptions carry no tick boxes, and a declined pull request can
never be reopened or edited: a `recreate` entry is the only way to rebuild a paused
branch, and a `forget_declines` entry the only way, besides editing `packs` or `ignore`,
to have declined content proposed again. touchmark cannot mark the declined pull request revoked there, so the entry acts
on every run while it is present: keep it until the pull request it brings is merged or
closed. Removed earlier, the decline is in force again; an open pull request stays, as
memory never closes one, and its description then names the decline under *Previously
declined*.

## On your machine

A maintainer who holds the write key can run the same operations as flags of
Expand Down
79 changes: 77 additions & 2 deletions docs/guide/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

One hub can deliver to GitHub, GitLab, Gitea and Forgejo at once, including
self-managed instances. Each platform is a **provider** in `hub.yml`, with its own reader
and writer; `targets.yml` names repositories by provider.
and writer; `targets.yml` names repositories by provider. Bitbucket Cloud is a provider
too, delivered to from a hub on another platform: see [Bitbucket Cloud](#bitbucket-cloud).

## One provider: the shorthand

Expand Down Expand Up @@ -58,7 +59,8 @@ providers:
- `id` is how `targets.yml` names the provider, and the `<ID>` of its variables:
upper-cased, with `-` as `_`.
- `api_url` is needed only when it cannot be derived: `api.github.com`, `<url>/api/v3`
for GitHub Enterprise Server, `/api/v4` for GitLab, `/api/v1` for Gitea and Forgejo.
for GitHub Enterprise Server, `/api/v4` for GitLab, `/api/v1` for Gitea and Forgejo,
`https://api.bitbucket.org/2.0` for Bitbucket Cloud.
- `known_authors` are former writers whose pull requests stay the hub's own: a replaced
App, a recreated group access token's bot, the account of a multi-gitter setup.
touchmark never adds anyone there itself.
Expand Down Expand Up @@ -124,6 +126,79 @@ on GitHub or GitLab can still deliver to them: add them as providers, and keep t
write token in the GitHub environment or the GitLab protected variable, where the probe
covers it.

## Bitbucket Cloud

A provider of type `bitbucket` is Bitbucket Cloud at `https://bitbucket.org` (leave `url`
out). `plan`, `distribute` and `doctor` work on it from a hub on GitHub or GitLab, or
wherever you run touchmark; a hub whose own CI is Bitbucket Pipelines is not supported
yet, and `setup` has nothing for it. Bitbucket Data Center is not supported.

```yaml
# hub.yml
providers:
- id: bb
type: bitbucket
writer: "{3f2a8d4e-1b6c-4f0a-9e7d-5c2b1a0f9e8d}" # the writer account's UUID, in braces
```

**Accounts.** Make two bot accounts (Atlassian accounts that only touchmark uses), each
with an API token; app passwords are gone, and repository, project and workspace access
tokens are not supported, since they cannot tell who they are. A token's scopes do not
narrow the repositories it reaches, the account's permissions do: that is why the writer
is an account of its own.

| | Reader | Writer |
|---|---|---|
| Variable | `TOUCHMARK_BB_READ_TOKEN` | `TOUCHMARK_BB_WRITE_TOKEN` |
| Scopes (all `…:bitbucket`) | `read:user`, `read:workspace`, `read:repository`, `read:pullrequest` | the reader's, plus `write:repository` and `write:pullrequest`; no admin or delete scope |
| Repositories | read access to the targets | write access to the targets only (directly, through a group or the project); not admin, and no access to the hub |

API tokens expire within a year, and Bitbucket's API shows neither their expiry nor
their scopes: `doctor` reports both as unknown, so keep a reminder to rotate them.

- **Accounts are UUIDs.** Bitbucket finds no account by its nickname, so `writer`,
`known_authors` and `automation_accounts` name accounts by their UUID in braces, as
`GET https://api.bitbucket.org/2.0/user` shows it.
- **Targets.** `org` is a workspace, `repo` is `workspace/repository`, and web URLs such as
`https://bitbucket.org/acme/billing` work. Bitbucket has no topics: `check` refuses
`topics` on a Bitbucket entry; select repositories with `match` (`acme/svc-*`) or list
them with `repo`.
- **No labels.** Bitbucket pull requests have none: `pr.labels` sets nothing there, and
touchmark records no label as set.
- **Drafts** are Bitbucket's own draft flag (`pr.draft`).
- **Closing.** touchmark closes its own pull request by declining it, after writing the
close into its description in the same edit. Bitbucket can never reopen a declined pull
request, nor change it: a pull request touchmark closed stays closed, and the next
proposal is a new one.
- **Declines** are remembered without writing to the declined pull request: while a pull
request is open, its marker records the state of the opt-in file it is proposed under
(touchmark brings it up to date when the file changes), and a person's decline holds
while the opt-in file is parsed the same. touchmark leaves no comment on it; the report
says `declined`. To have declined content proposed again, edit `packs` or `ignore`, or
add a `forget_declines` entry, which acts while it is present on Bitbucket (see
[Memory of declined pull requests](../concepts/memory.md#on-bitbucket-cloud)).
- **No tick boxes, no HTML.** *Rebuild this branch* and *Propose this content again* rest
on HTML comments, which Bitbucket shows as text: descriptions there carry neither. A
paused pull request asks for a `recreate` entry in `.touchmark/operations.yml` instead,
and the footnote names `forget_declines`. The files the repository made its own are a
plain section, not a folded one. Keep `pr.intro_file` free of HTML too: Bitbucket shows
it as text.
- **Branch restrictions** are readable with admin rights only, which the writer should
not have: `doctor` shows `rules` as unknown, and a push a restriction refuses
("Permission denied to update branch") is `blocked:rules:protected-branch`.
- **The marker.** Bitbucket shows HTML comments in descriptions as text, so the marker
is a Markdown link reference definition there, which renders as nothing:
`[touchmark]: # "touchmark:v1 hub=… fp=… stream=sync key=… data=…"`, the same payload as
the comment elsewhere. An edit on the website may add backslashes before punctuation
and Windows line endings; touchmark reads the marker either way.
- **Commit author.** Bitbucket links a commit to the account one of whose confirmed
addresses is the author's: touchmark authors the writer's commits with its account's
primary confirmed address, and with `<uuid>@touchmark.invalid` (an address that can
belong to no one) when the account has none.
- **Pace.** Bitbucket allows an account about 1 000 API requests an hour, so touchmark
reads 15 times a minute by default, with two targets at once, and writes at most once a
second; `limits` overrides it.

## Moving

- **A target moves to another provider on the same host** (a new App, a new group): add
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ changelog before you merge it.
(`touchmark schema <name>`): the schemas published on `master` describe the next release
and reject fields they do not know.
- **What touchmark leaves in repositories:** the marker in pull request bodies
(`touchmark:v1`), the commit trailers (`Touchmark-Hub`, `Touchmark-Stream`,
(`touchmark:v1`, an HTML comment, or a Markdown reference definition on Bitbucket
Cloud), the commit trailers (`Touchmark-Hub`, `Touchmark-Stream`,
`Touchmark-Content`, `Touchmark-Hub-Commit`), the control lines of a pull request body,
the marker of the plan comment, and the sync branch's name. Every release reads what an
earlier one wrote. The marker's payload is frozen: a change to it is a `touchmark:v2`
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ history. `touchmark manifest` prints it.

**marker** — the HTML comment at the end of a sync pull request's description:
`<!-- touchmark:v1 hub=… fp=… stream=sync key=sha256:… data=… -->`. It records the
fingerprint and what the pull request carries.
fingerprint and what the pull request carries. On Bitbucket Cloud, which shows HTML
comments as text, the same payload is a Markdown link reference definition:
`[touchmark]: # "touchmark:v1 hub=… fp=… stream=sync key=sha256:… data=…"`.

**opt-in file** — `.engineering-assets.yml` at a target's root. Its presence is consent;
it can add packs and `ignore` paths, and `enabled: false` in it opts the target out.
Expand Down
Loading
Loading