Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ Three ways in, one version number:
- **Review, never merge** — every change arrives as a pull request a person merges; there
is no auto-merge option
- **Opt-in** — a repository receives nothing until it adds `.engineering-assets.yml`, which
can add packs and `ignore` paths
can add packs and `ignore` paths; a hub that owns the decision subscribes repositories
itself (`opt_in: assumed`), and `enabled: false` in the file still opts one out
- **Targets as you name them** — a repository, an organisation or a group by path or by
web URL, narrowed by topics and path globs (`acme/svc-*`), minus `exclude` globs
(`corp:platform/legacy/**`)
- **Four platforms, one hub** — GitHub (github.com, GHE.com, Enterprise Server), GitLab
(gitlab.com and self-managed), Gitea and Forgejo; one hub can deliver to several at once
- **Declines remembered** — content in a pull request closed without merging is not
Expand Down Expand Up @@ -222,9 +226,9 @@ every flag.
| File | Lives in | What it holds |
|---|---|---|
| `hub.yml` | the hub | the hub's id, the platforms it delivers to with their writers, commit and pull request settings, security settings, pack descriptions, `requires` and `formerly` |
| `targets.yml` | the hub | which repositories get which packs: by repository, organisation or group, filtered by topic |
| `targets.yml` | the hub | which repositories get which packs: by repository (its path or web URL), organisation or group, filtered by topic and path pattern, minus `exclude` patterns; and whether the hub subscribes them without an opt-in file (`opt_in: assumed`) |
| `.touchmark/operations.yml` | the hub | one-off overrides of a safeguard, merged through review: rebuild a paused branch, propose declined content again, allow a mass close |
| `.engineering-assets.yml` | each target | the repository's consent; packs to add and paths to `ignore` |
| `.engineering-assets.yml` | each target | the repository's consent, or with `enabled: false` its refusal; packs to add and paths to `ignore` |

Credentials come from the environment, never from flags or files:
`TOUCHMARK_[<ID>_]READ_TOKEN` (or `…_READ_APP_ID` with `…_READ_APP_KEY` for a GitHub App),
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ inputs:
required: false
default: "false"
assume-opt-in:
description: "plan: report as if every target had opted in (--assume-opt-in). true or false."
description: "plan: report as if every target without an opt-in file had opted in (--assume-opt-in); enabled: false still opts a target out. true or false."
required: false
default: "false"
dry-run:
Expand Down
47 changes: 34 additions & 13 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ file is copied byte for byte. It does not merge a team's edits into a pack's new
an edited file is the team's and is never touched again. It keeps no state in targets —
no lock file, no header, no marker in their files. It never checks a target out in CI,
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. There is no pull mode (targets do not
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.

## Mental model
Expand Down Expand Up @@ -75,7 +76,17 @@ configures it: `hub.yml`, `targets.yml`, `.touchmark/operations.yml`, the CI fil
later wins. The hub sets the minimum; a target can add packs and `ignore` paths, never
remove a pack the hub assigns.
* **Opt-in.** A target receives nothing until `.engineering-assets.yml` exists at its
root. An empty file is consent.
root. An empty file is consent. An entry of `targets.yml` with `opt_in: assumed`
subscribes the repositories it selects instead: they count as opted in without the
file, as if it were empty; one such entry is enough. The file still wins:
`enabled: false` in it opts any repository out (`skipped:opted-out`, its open sync pull
request closed), and deleting it returns a subscribed repository to the hub's
subscription rather than opting it out.
* **Targets** are named `[<provider>:]<path>` or by web URL (`https://host/owner/name`),
whose provider is the one in `hub.yml` at that url; output and `--only` always use
`<provider>:<path>`. `exclude` entries and the `match` filter of `org`/`group` entries
are globs: `*` within a segment, `**` across segments, `?` one character, case
ignored; YAML needs a glob that starts with `*` quoted (`"**/legacy"`).
* **Accounts.** A **reader** (read-only) runs `plan` on hub pull requests. A **writer**,
a different account, runs `distribute` and `doctor` from the hub's default branch only.
The **hub channel**, the CI's own token (`GITHUB_TOKEN`, `CI_JOB_TOKEN`), reaches the hub
Expand Down Expand Up @@ -253,14 +264,22 @@ ten `::error` and ten `::warning` annotations.
| `packs.<pack>.description`, `.requires`, `.formerly` | none | metadata; `requires` adds and orders dependencies; `formerly` keeps a renamed pack's history |

`targets.yml` (in the hub; `touchmark schema targets`): `defaults.provider`,
`defaults.packs`; `targets[]` entries with exactly one of `repo` (`owner/name`, or
`provider:path`), `org` or `group` (a namespace; with `topics` that must all match,
`subgroups` default `true`, `forks` default `false`), each with optional `provider` and
`packs`; `exclude[]`.
`defaults.packs`, `defaults.opt_in`; `targets[]` entries with exactly one of `repo`
(`owner/name`, `provider:path`, or a web URL), `org` or `group` (a namespace, by path or
web URL; with `topics` that must all match, `subgroups` default `true`, `forks` default
`false`, `match` a list of globs over the full path of which one must match), each with
optional `provider`, `packs` and `opt_in` (`required`, the default, or `assumed`);
`exclude[]` (repositories, globs or web URLs; it wins over every entry). A URL resolves to
the one provider whose `url` it lies under (`github` defaults to `https://github.com`,
`gitlab` to `https://gitlab.com`); none fails `check`, and so do several unless the
entry's `provider` or `defaults.provider` picks one of them, credentials, a query, a
fragment, and any scheme but `https` (`http` for loopback only). On GitHub, Gitea and
Forgejo a repository URL, and an `exclude` URL without `**`, names `owner/name`.

`.engineering-assets.yml` (in each target; `touchmark schema opt-in`): `version`,
`packs` (added to the hub's), `ignore` (paths or globs, `**` for any depth, at most
1000; a plain path covers everything under it). At most 64 KiB.
`enabled` (default `true`; `false` opts out whatever the hub says), `packs` (added to
the hub's), `ignore` (paths or globs, `**` for any depth, at most 1000; a plain path
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),
Expand Down Expand Up @@ -301,8 +320,10 @@ from its environment once read:
5. **The writer has no access to the hub.** Install the Apps on targets only; keep the
hub outside the GitLab groups the accounts belong to. `doctor` fails when the writer
can push to the hub, and warns when it can see a private one.
6. **Nothing reaches a repository without its opt-in file.** `.engineering-assets.yml`
at the root, possibly empty. Never ship that file in a pack: `check` rejects it.
6. **Nothing reaches a repository without its consent.** `.engineering-assets.yml` at
the root, possibly empty, or an entry with `opt_in: assumed` when the hub owns the
decision; `enabled: false` in the file overrides the hub. Never ship that file in a
pack: `check` rejects it.
7. **Only committed packs ship.** `distribute` reads the hub's HEAD commit and refuses
`--worktree`; it runs only when HEAD is the tip of the default branch (otherwise the
run is `superseded` and writes nothing).
Expand Down Expand Up @@ -436,7 +457,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`, `archived`, `disabled`, `empty`, `mirror`, `pending-deletion`, `prs-disabled`, `sha256`, `unsafe-opt-in`, `private-in-public-hub`, `superseded` | add the opt-in file, or nothing |
| `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` |
| `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 All @@ -457,10 +478,10 @@ Fetch a page when the task is the one named beside it.
| [A hub on GitHub](getting-started/github.md) | creating a hub with GitHub Apps and the `touchmark-distribute` environment |
| [A hub on GitLab](getting-started/gitlab.md) | creating a hub with service accounts and protected variables |
| [A hub on Gitea or Forgejo](getting-started/gitea-forgejo.md) | delivering to or hosting on Gitea and Forgejo |
| [Opt a repository in](getting-started/opt-in.md) | writing a target's `.engineering-assets.yml` |
| [Opt a repository in](getting-started/opt-in.md) | writing a target's `.engineering-assets.yml`; a hub that subscribes repositories (`opt_in: assumed`) and how they opt out |
| [How it works](concepts/overview.md) | the flow from a hub pull request to a sync pull request |
| [Ownership by provenance](concepts/ownership.md) | the file states, `local`, `--adopt`, CRLF, `orphaned` |
| [Packs and opt-in](concepts/packs.md) | pack selection order, `requires`, `formerly`, seed files |
| [Packs and opt-in](concepts/packs.md) | pack selection order, `requires`, `formerly`, seed files, consent by file or by the hub |
| [Delivery and the sync branch](concepts/delivery.md) | the branch, the commit, signing, pull request fields, stale pull requests |
| [Memory of declined pull requests](concepts/memory.md) | why a pull request did not come back, and how to bring it back |
| [Security model](concepts/security.md) | the accounts, the probe, write isolation, what a target can and cannot do |
Expand Down
7 changes: 5 additions & 2 deletions docs/concepts/delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ One pull request per target, from the sync branch to the default branch.

| Field | Belongs to | touchmark writes it |
|---|---|---|
| body | touchmark | when the changes it proposes change, and to record a decline, a close or a rebuild |
| body | touchmark | when the changes it proposes change, and to record a decline, a close or a rebuild; also when a repository the hub subscribed (`opt_in: assumed`) gains or loses its opt-in file, which adds or drops the subscription paragraph |
| title | people, after creation | at creation; later only if `pr.title` changed and nobody edited the title |
| draft | people, after creation | at creation (`pr.draft`) |
| labels | shared | at creation (`pr.labels`); later only labels it never set; never removes one |
Expand All @@ -94,6 +94,9 @@ One pull request per target, from the sync branch to the default branch.
The body, in English:

1. the text of `pr.intro_file`, then the hub, the hub commit and the packs;
for a repository the hub subscribed without an opt-in file, a paragraph on how to
choose packs or keep files out (add the opt-in file with `packs` or `ignore`) and how
to opt out (`enabled: false` in it). Never cut;
2. **⚠ Sensitive paths**: changes to workflows, CI configuration, agent settings, skills,
subagents, hooks, CODEOWNERS, `.gitattributes` and executable files (plus
`sensitive_paths` from `hub.yml`). Never cut;
Expand Down Expand Up @@ -122,7 +125,7 @@ other pull request.
| Reason | When |
|---|---|
| `no-diff` | the target already has everything; the branch is deleted |
| `opted-out` | the opt-in file was deleted |
| `opted-out` | the repository is no longer opted in: its opt-in file says `enabled: false`, or it has no opt-in file and the hub does not subscribe it (the file was deleted, or the hub dropped `opt_in: assumed`); the comment says which |
| `target-dropped` | the target left `targets.yml` or entered `exclude` |
| `duplicate` | two of touchmark's pull requests are open, on the branch and on an alias |

Expand Down
4 changes: 3 additions & 1 deletion docs/concepts/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ covered by the target's declines. It does open one when anything is new:
| #7: A created, B created | B was edited in the target (it is `local`) | a new pull request with A |
| #7: A created, B created | B added to `ignore` | a new pull request with A |
| #7: A created | a comment changed in the opt-in file | `declined`: the parsed file is the same |
| #7: A created, in a repository the hub subscribed without an opt-in file | an empty opt-in file added | `declined`: the file is parsed the same as the empty one assumed |
| #7: A created; #9: B created | nothing | `declined`: both pairs are covered |
| #7: A created, B created | the pack changes B | a new pull request with A and the new B, saying *A was declined in #7* |
| #7: A at v1 | A at v2 was merged, then the hub went back to v1 | a new pull request: `v2 → v1` is another pair |
Expand Down Expand Up @@ -67,7 +68,8 @@ To undo a decline:
- **from the hub**, add a `forget_declines` entry to `.touchmark/operations.yml` and merge
it through review (see [One-off operations](../guide/operations.md)).

To refuse a file for good instead, add it to `ignore`, or delete the opt-in file to stop
To refuse a file for good instead, add it to `ignore`, or set `enabled: false` in the opt-in
file (deleting it works too, unless the hub subscribed the repository) to stop
receiving anything.

## Limits
Expand Down
10 changes: 5 additions & 5 deletions docs/concepts/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ your-org/engineering-assets
reports what each would receive: pull requests to open, update and close, the
sensitive paths, the API writes it would cost. Nothing is written.
2. **A maintainer merges it.** On the default branch, `touchmark distribute` runs with
the write account. For every target that has opted in and has a difference, it
builds one commit on the sync branch `touchmark/<id>` and opens or updates one pull
request. A second run on the same inputs writes nothing.
the write account. For every target that has opted in (or that the hub subscribes)
and has a difference, it builds one commit on the sync branch `touchmark/<id>` and
opens or updates one pull request. A second run on the same inputs writes nothing.
3. **A person in each target reviews it.** They merge it, or edit files (which makes
them the repository's own), or close it, which touchmark remembers as a decline.

Expand All @@ -40,8 +40,8 @@ before writing, and its report is the same as `plan`'s.
| Phase | What happens |
|---|---|
| guards | git version, configuration, `check`, the write isolation probe, the hub's fingerprint; the hub's HEAD must be the tip of its default branch, or the run is `superseded` and writes nothing |
| resolve | `targets.yml` becomes a list of repositories: organisations and groups listed, topics filtered, `exclude` applied, duplicates merged by repository id |
| inspect | per target, in parallel: archived, empty or mirror repositories skipped; the opt-in file read; the target's tree fetched without blobs or a checkout; the decision made |
| resolve | `targets.yml` becomes a list of repositories: web URLs matched to providers, organisations and groups listed, `topics` and `match` filtered, `exclude` patterns applied, duplicates merged by repository id |
| inspect | per target, in parallel: archived, empty or mirror repositories skipped; the opt-in file read (an empty one assumed where `targets.yml` subscribes the target; `enabled: false` skips it); the target's tree fetched without blobs or a checkout; the decision made |
| sweep | sync pull requests of this hub in repositories that are no longer targets are found, to be closed |
| gate | the guards against mass actions: at most `limits.max_new_prs_per_run` new pull requests, and no mass close |
| execute | `distribute` only: closes, then updates, then new pull requests, under each provider's write budget |
Expand Down
24 changes: 23 additions & 1 deletion docs/concepts/packs.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ exception is visible in the target itself.
## Opt-in

A target receives nothing until it has the opt-in file, `.engineering-assets.yml` at its
root (`opt_in_file` in `hub.yml` renames it). An empty file is consent.
root (`opt_in_file` in `hub.yml` renames it), or the hub subscribes it (below). An empty
file is consent.

```yaml
version: 1
Expand All @@ -68,10 +69,31 @@ ignore:
directories.
- Editing `packs` or `ignore` is a new choice of the team, so it lifts every decline the
repository made: see [memory](memory.md).
- `enabled: false` opts the repository out, whatever the hub says.

The opt-in file belongs to each repository: `touchmark check` rejects a pack that ships
it.

### When the hub subscribes

Consent by file suits a hub that offers packs to teams. A hub that keeps its own
organisation's repositories in line can subscribe them instead: `opt_in: assumed` on an
entry of `targets.yml` (or in its `defaults`) makes every repository the entry selects
count as opted in without the file, as if it had an empty one.

| The repository has | It gets |
|---|---|
| no opt-in file, and no entry that selects it says `assumed` | nothing (`skipped:not-opted-in`) |
| no opt-in file, and an entry that selects it says `assumed` | the packs of `targets.yml`, nothing ignored |
| an opt-in file | the packs of `targets.yml` and of the file, minus what it ignores |
| an opt-in file with `enabled: false` | nothing (`skipped:opted-out`); its open sync pull request is closed |

One entry with `assumed` is enough: entries add up, as their packs do. The consent moves,
the choice does not: the first sync pull request of a subscribed repository says the hub
subscribed it and how to opt out, and closing it is a decline like any other. Deleting
the opt-in file of a subscribed repository returns it to the hub's subscription; only
`enabled: false` opts it out. See [targets.yml](../reference/targets.md#opt-in).

## Renaming and retiring

- **Rename** a pack by moving its directory and listing the old name under `formerly`.
Expand Down
Loading
Loading