Your organisation's engineering standard, delivered to every repository as pull requests: agent instructions, review prompts, merge request templates and shared CI files.
This is a template. Create your own hub from it, put the files your teams share into packs, and list the repositories they belong to. Every change to a pack then arrives in those repositories as a pull request (a merge request on GitLab) that a person reviews and merges.
The work is done by touchmark (documentation). It only ever touches files it can prove it shipped: a file a team has changed stays theirs.
your-org/engineering-assets this hub: packs/, hub.yml, targets.yml
│ one pull request per repository on every change to a pack
▼
your-org/billing, your-org/sdk-python, … opted in, or subscribed by the hub
packs/ the files you deliver, one directory per pack
agents/ AGENTS.md, a project profile to fill in, review and refactoring prompts
claude/ Claude Code instructions, skills, a reviewer subagent, settings
python-service/ guidelines for Python services; skills to run, test and format
python-library/ guidelines for Python libraries and their public API
gitlab/ merge request and issue templates
hub.yml hub id, write account, security settings, pack descriptions and dependencies
targets.yml which repositories to visit and which packs they get (empty)
.touchmark/operations.yml one-off operations, reviewed like any other change (empty)
.github/workflows/engineering-assets.yml GitHub Actions: check and plan on pull requests, distribute and doctor
.github/dependabot.yml updates the pinned actions, touchmark among them
.github/CODEOWNERS who reviews changes to the hub, on GitHub
.gitlab-ci.yml the same jobs on GitLab CI
.gitlab/CODEOWNERS code owners on GitLab
.gitea/workflows/ the same jobs on Gitea and Forgejo Actions
.gitea/CODEOWNERS code owners on Gitea and Forgejo
bitbucket-pipelines.yml the same jobs on Bitbucket Pipelines
azure-pipelines.yml the same jobs on Azure Pipelines
.azure-pipelines/touchmark.yml the touchmark step azure-pipelines.yml runs, with the pinned image
renovate.json updates the pinned touchmark image (GitLab, Gitea, Forgejo, Bitbucket, Azure)
scripts/validate.sh runs touchmark check and actionlint before you push
LICENSE MIT-0
Only packs/ is delivered; everything else configures the hub. The starter packs are yours: edit them, delete them, add your own. Nothing in the hub updates itself from upstream. Only the touchmark engine does, through a Dependabot or Renovate pull request that you review.
The CI files of all platforms ship together, and each platform reads only its own. You may delete the ones you don't use, with their CODEOWNERS.
touchmark setup github, run from a clone of the hub with an administrator's token, does most of steps 2 to 4: it creates both Apps, the environment and the ruleset, and prints the commands that store the keys. You install the Apps. See Set up a hub's platform.
-
Create the hub. Click Use this template and create
your-org/engineering-assets. It can be private, but see Security for GitHub Free. Don't fork: forks of public repositories are always public. -
Create two GitHub Apps in your organisation, and install both on the repositories you will target, not on the hub:
- reader, with Metadata, Contents and Pull requests read-only;
- writer, with Metadata read-only, and Contents, Pull requests and Workflows read and write. touchmark asks for Workflows only for a target that needs it.
-
Store the keys.
- Reader: the repository variable
TOUCHMARK_READ_APP_ID(the App ID) and the repository secretTOUCHMARK_READ_APP_KEY(its private key). - Writer: create an environment named
touchmark-distribute. Set its Deployment branches and tags to Selected branches and tags, with your default branch only. Store the variableTOUCHMARK_WRITE_APP_IDand the secretTOUCHMARK_WRITE_APP_KEYin that environment, and nowhere else. Don't choose Protected branches only: with no protection rules, every branch qualifies.
The workflow's
probejob stopsdistributeif the write key is also visible as a repository or organisation secret, and touchmark itself checks the environment's branch policy before it writes. - Reader: the repository variable
-
Protect the hub. Add a ruleset for the default branch that requires a pull request with an approving review and a review from Code Owners. Replace
@acme/hub-maintainersin.github/CODEOWNERSwith your team. -
Describe your hub. In
hub.ymlsetid, andwriterto the writer App's bot name, likeacme-assets-write[bot]. List your repositories intargets.yml. -
Open a pull request. The
checkjob validates the hub; theplanjob reports what each repository would receive and keeps the report in a comment. Write access is checked whendistributeruns, and every week bydoctor. -
Merge it. The
distributejob opens a pull request in every repository that has opted in. It runs again every day and on every merge; Run workflow runsdistributeanddoctorat once. The workflow starts on pushes tomainandmaster: if your default branch has another name, add it underpush: branches:.
The jobs run on ubuntu-latest. On self-hosted runners, the touchmark Action needs Linux with Docker, docker buildx and the GitHub CLI (gh), which it uses to check the image's build provenance, and HTTPS access to ghcr.io and to the Sigstore trust roots gh fetches for that check (tuf-repo-cdn.sigstore.dev and tuf-repo.github.com).
touchmark setup gitlab --group <your group>, run from a clone of the hub with the token of a Maintainer of the hub who owns that group, does steps 2 to 4 through the API. See Set up a hub's platform.
- Create the hub. Choose New project → Import project → Repository by URL and give this repository's URL,
https://github.com/bedrock-python/engineering-assets-template.git. - Create two service accounts and add them to the groups that own your repositories. On instances without service accounts, use group access tokens. Keep the hub out of those groups: put it in a group or subgroup the accounts don't belong to, and add the accounts, or create the group access tokens, only on groups that hold targets and not the hub. A member of a group inherits its projects, and a group access token is a member of its group, so a writer on a group that holds the hub could push to it;
touchmark setup gitlabrefuses that layout, anddoctorfails on it. If the hub has to stay in that group, setsecurity.writer_on_hub: guardinhub.ymlfirst: the writer is then a Developer of the hub, and setup anddoctormake sure it can neither push nor merge to the default branch, that merge requests run the default branch's CI file and wait for it, andplanrefuses a merge request the writer opened or pushed to (A writer on the hub).- The reader has the role Reporter and a token with
read_apiandread_repository. Store it as the variableTOUCHMARK_READ_TOKEN, masked but not protected: merge request pipelines need it. - The writer has the role Developer and a token with
apiandwrite_repository. Store it asTOUCHMARK_WRITE_TOKEN: Protected, Masked and hidden, environment scopetouchmark-distribute. Don't add the writer to the hub project.
- The reader has the role Reporter and a token with
- Lock down the hub.
- Protect only the default branch (Allowed to push: No one) and don't create protected tags.
- Set Minimum role to use pipeline variables to No one allowed, and keep Allow merge request pipelines to access protected variables off.
- The
probejob fails a merge request pipeline that can see the write token. Runtouchmark doctor --hub-tokenonce with a Maintainer token to check the variable's settings. - Replace
@acme/hub-maintainersin.gitlab/CODEOWNERSwith your group. Requiring code owner approval needs Premium. - Every job runs in the touchmark image, so the hub's runners must honor
image:: GitLab.com's hosted runners do, and so do the Docker, Docker Autoscaler and Kubernetes executors. The Shell executor ignoresimage:, and the jobs fail. - If your runners can't pull from ghcr.io, change the image in
.gitlab-ci.ymlin a reviewed merge request. The image is pinned by digest on purpose: an overridable variable could leak the token.
- Schedule the runs. Under Build → Pipeline schedules, create two schedules for the default branch: a daily one described
touchmark distributeand a weekly one describedtouchmark doctor. Thedoctorjob runs in schedules whose description contains "doctor"; every other schedule runsdistribute. - Describe your hub. In
hub.ymlsetid, andwriterto the writer's username. For a self-managed instance, describe it underprovidersinstead, with itsurlandwriter, so that touchmark also knows it outside CI. List your repositories intargets.yml. - In your target projects, turn on the CI/CD job token allowlist (Limit access to this project). Sync pipelines run as the writer, so their job token carries the writer's access to your other projects. Use a separate writer for groups you trust differently.
- Open a merge request and check the
check,planandprobejobs. The plan report is attached to the merge request. - Merge it. The
distributejob opens a merge request in every project that has opted in. Run pipeline on the default branch runsdistributeanddoctor.
Gitea and Forgejo Actions can't limit a secret to the default branch, so touchmark won't use their CI with security.write_isolation: platform. Pick one:
- run the hub's CI on GitHub or GitLab, and deliver to Gitea or Forgejo from there: add them as
providersinhub.ymland their keys next to the others; - host the hub on Gitea or Forgejo, set
security.write_isolation: nonewith areason, and protect every branch (*) so that only maintainers can push. Contributors then work from forks, which receive no secrets.
For a hub on Gitea or Forgejo:
- Create the hub by migrating this repository.
- Create two bot users: a reader in a team with read access and a writer in a team with write access to your repositories. Give the teams your target repositories one by one, not all repositories of the organisation, which would include the hub. Reader token:
read:repository, read:issue, read:organization, read:user. Writer token:write:repository, write:issue, read:organization, read:user. Never use an admin token. Store them as the Actions secretsTOUCHMARK_READ_TOKENandTOUCHMARK_WRITE_TOKEN. - Check the runner label. The workflows in
.gitea/workflows/run onubuntu-latest, the label of Gitea's default runners. If your runners are registered with other labels, changeruns-on. Every job runs in the touchmark image and clones the hub with git, so it needs no actions from github.com or a mirror. The report is in the job's log, and on Gitea 1.27 with runner 2.0 in its summary. - Protect the hub as above, and replace
@acme/hub-maintainersin.gitea/CODEOWNERSwith your team. - Describe your hub. Set
idandwriterinhub.yml, then open a pull request and merge it.distributeruns on every merge tomainormaster(add your default branch underpush: branches:if it has another name) and every day;doctorevery Monday, from.gitea/workflows/engineering-assets-doctor.yml.
Not verified on a live Bitbucket yet. The Bitbucket support follows Atlassian's documentation and is tested against a stand-in of its API. The points a first live run must confirm are in A hub on Bitbucket Cloud.
Bitbucket Pipelines gives a deployment environment's variables to every step that names the environment, in any branch's bitbucket-pipelines.yml. Only the environment's deployment permissions, a Premium feature, stop the steps of other branches, and Bitbucket's API does not show them, so touchmark cannot check them. Pick one:
- Premium: keep
security.write_isolation: platform, restrict the environmenttouchmark-distributeto the default branch and to admins or selected users, and say so insecurity.reason. Without thatreason, touchmark refuses to rundistributeon Bitbucket. - Free or Standard: set
security.write_isolation: nonewith areason. Every branch of the hub can then read the write key: let only maintainers push, and keep write access to the hub tight. Or run the hub's CI on GitHub or GitLab, and deliver to Bitbucket Cloud from there.
touchmark setup has nothing for Bitbucket yet, so these steps are by hand:
-
Create the hub. In your workspace choose Create → Repository → Import repository, give this repository's URL,
https://github.com/bedrock-python/engineering-assets-template.git, and turn Pipelines on (Repository settings → Pipelines → Settings). -
Create two bot accounts with an API token each: a reader with read access to your target repositories and the scopes
read:user,read:workspace,read:repositoryandread:pullrequest, and a writer with write access to the targets only (no admin, no access to the hub) and those scopes pluswrite:repositoryandwrite:pullrequest(all…:bitbucket). -
Create the hub's access token (Repository settings → Security → Access tokens) with Repositories: Read and Pull requests: Write. Pipelines gives a step no API token: touchmark reads the default branch with this one, and keeps
plan's comment with it. -
Store the keys, all Secured:
- repository variables
TOUCHMARK_READ_TOKEN(the reader's token) andTOUCHMARK_PIPELINES_TOKEN(the hub's access token); - create the deployment environment
touchmark-distribute(Repository settings → Deployments) and store the writer's token as its variableTOUCHMARK_WRITE_TOKEN, and nowhere else. On Premium, allow deployments to it from the default branch only, and only by admins (Only allow admins to deploy) or selected users or groups: the branch alone is not enough, since anyone with write access to the hub can run the custom pipelines on the default branch with variables of their choosing.
The
probestep runs first in every pipeline, without the deployment, and stops the pipeline if it can see the write token: then every branch could. - repository variables
-
Protect the hub. Under Branch restrictions, let no one push to the default branch directly and only maintainers merge into it; keep the bot accounts and the access token out. Bitbucket reads none of the CODEOWNERS files: replace
@acme/hub-maintainersin them, or delete them with the other platforms' CI files. -
Schedule the runs. Under Pipelines → Schedules, on the default branch: a daily schedule for the custom pipeline
distribute, and a weekly one fordoctor. -
Describe your hub. In
hub.ymlsetid,writerto the writer account's UUID in quotes, like"{3f2a8d4e-…}", andsecurityas above. Intargets.ymluseorg: <workspace>withmatch, orrepo: <workspace>/<repository>: Bitbucket has no topics. -
Open a pull request and check the
probe,checkandplansteps.plankeeps its report in a comment; the report files are the step's artifacts. -
Merge it. The push pipeline runs
probe, thendistribute, which opens a pull request in every repository that has opted in. Run pipeline runs the custom pipelinesdistributeanddoctor. The push pipeline starts onmainandmaster: if your default branch has another name, add it to thebranchespattern.
Not verified on a live Azure DevOps yet. The Azure Pipelines support follows Microsoft's documentation and is tested against a stand-in of its API. The points a first live run must confirm are in A hub on Azure DevOps.
The hub lives in Azure Repos of Azure DevOps Services, and azure-pipelines.yml runs touchmark with docker on Microsoft-hosted Ubuntu agents. Any branch's copy of that file could link the variable group that holds the write key: the group's Branch control check, admitting the default branch only, is what keeps every other branch out. touchmark cannot read that check from the job, so under security.write_isolation: platform it refuses to run distribute until security.reason states the check; touchmark doctor --hub-token then verifies it with a maintainer's token.
touchmark setup has nothing for Azure DevOps yet, so these steps are by hand:
-
Create the hub. In your project choose Repos → Import repository and give this repository's URL,
https://github.com/bedrock-python/engineering-assets-template.git. Then Pipelines → New pipeline → Azure Repos Git, pick the hub and Existing Azure Pipelines YAML file,/azure-pipelines.yml, and save it without running. -
Create two bot users in the organization (Basic access), each with a personal access token for this organization only: a reader with read access to your target repositories and the scope Code (read), and a writer with Contribute, Create branch and Contribute to pull requests on the targets only (no administration, no access to the hub) and Code (read & write). Tokens expire: note when.
-
Store the keys, all secret:
- the pipeline's variable
TOUCHMARK_READ_TOKEN(the reader's token; Edit → Variables, Keep this value secret, and not Let users override this value); - the variable group
touchmark-distribute(Pipelines → Library) with the writer's token asTOUCHMARK_WRITE_TOKEN, and nowhere else. Under its Pipeline permissions allow the hub's pipeline alone, and under Approvals and checks add Branch control with Allowed branchesrefs/heads/main(your default branch), Verify branch protection on and Allow unknown status off.
The
probestage runs first in every run, without the group, and stops the run if it can see the write token: then every branch could. - the pipeline's variable
-
Create the environment
touchmark-distribute(Pipelines → Environments, no resources):distributeanddoctorrun as deployment jobs to it, and touchmark refuses them anywhere else. Add the checks Exclusive lock, so that two runs never overlap, and the same Branch control. -
Protect the hub. Under Project settings → Repositories → the hub → Policies, on the default branch: add Build validation with the hub's pipeline, required (that is what runs the pull request builds: Azure Repos ignores
pr:triggers); add your maintainers as automatically included reviewers, required; require a minimum number of reviewers with Reset all approval votes on new changes, and Prohibit the most recent pusher from approving. A pull request build runs its own branch's YAML with the job access token, so a review policy that any vote satisfies is not enough. Azure DevOps reads none of the CODEOWNERS files: delete them with the other platforms' CI files. Under Organization settings → Pipelines → Settings turn on Limit variables that can be set at queue time (otherwise whoever may queue a run of the default branch sets variables in the steps that hold the write key), Limit job authorization scope to current project and Protect access to repositories in YAML pipelines. -
Optionally, let the pipeline comment.
plankeeps its report in a pull request comment with the job's own token only if<project> Build Service (<organization>)may Contribute to pull requests on the hub (Project settings → Repositories → the hub → Security). That right also lets a pull request build vote on pull requests, so grant it only with your maintainers as required reviewers (step 5). Without it the report is still in the run's summary and artifacts. -
Describe your hub. In
hub.ymlsetid,writerto the writer user's identity id (GET https://dev.azure.com/<organization>/_apis/connectionDatawith its token shows it asauthenticatedUser.id), and insecuritykeepwrite_isolation: platformwith areasonthat states the Branch control check of step 3. Intargets.ymluseorg: <organization>withmatch: ["<project>/*"], orrepo: <project>/<repository>: Azure DevOps has no topics. -
Open a pull request and check the
probe,checkandplanstages. The first run asks you to permit the pipeline to use the variable group and the environment. The report files are the run's artifacts, its summary shows them, and with step 6planalso keeps the report in a closed comment thread.touchmark doctor --hub-token --hub-fp dev.azure.com/<repository id>, from a clone of the hub with a maintainer's token, checks steps 3 to 5. -
Merge it. The run of the default branch does
probe, thendistribute, which opens a pull request in every repository that has opted in. The schedules rundistributeevery day anddoctorevery Monday; Run pipeline asks which of the two to run. The triggers and conditions namemainandmaster: if your default branch has another name, add it there.
Nothing happens to a repository until it has .engineering-assets.yml at its root, unless the hub subscribes it (below). The file can be empty: its presence is the consent. It can also ask for more packs and keep some paths for itself, or say enabled: false to opt out:
version: 1
packs: [claude] # in addition to what the hub assigns
ignore:
- .claude/settings.json # we keep our own
- .agents/guidelines/**Editing packs or ignore in this file is also how a team changes its mind: content it declined by closing a sync pull request is proposed again after the change. Comments and formatting don't count.
The hub decides the minimum every target gets in targets.yml. A repository can add packs and ignore paths, but it can't remove a pack the hub assigns.
# targets.yml
version: 1
defaults:
packs: [agents]
targets:
- repo: your-org/billing
packs: [python-service]
- org: your-org # on GitLab: group: your-org/platform
topics: [python-library]
packs: [python-library]
- repo: corp:platform/api # a repository on another provider from hub.yml
- repo: https://gitlab.example.com/platform/web # or its web URL
- org: your-org
match: [your-org/svc-*] # only the repositories whose path matches
opt_in: assumed # subscribed by the hub: no opt-in file needed
exclude:
- your-org/*-archive # * within a segment, ** across segments, ? one character
- corp:platform/legacy/**A web URL names a repository or a namespace of the provider in hub.yml whose url it lies under; reports name every target <provider>:<path>.
When the hub subscribes. An entry with opt_in: assumed (or defaults.opt_in: assumed) makes the repositories it selects count as opted in without the file: they get the packs of targets.yml, and their first sync pull request says the hub subscribed them. Closing it is a decline, remembered as for any target. A team chooses packs or ignores files by adding .engineering-assets.yml, and opts out with enabled: false in it; deleting the file of a subscribed repository returns it to the hub's subscription.
| Pack | What a repository gets | Needs |
|---|---|---|
agents |
AGENTS.md, the instructions every coding agent reads; .agents/project.md, a project profile for the team to fill in; the prompts .agents/prompts/review.md (a review), refactor-check.md (before a refactoring) and pure-refactor.md (a check that a change keeps behaviour) |
— |
claude |
CLAUDE.md, which imports AGENTS.md and the profile; .claude/settings.json, which only denies reading secrets; the skills /review-change, /spec, /new-branch, /commit and /pr; the reviewer subagent |
agents |
python-service |
.agents/guidelines/python.md, testing.md and database.md; the skills /app-start, /app-stop, /test and /fmt; the test-runner subagent |
agents |
python-library |
.agents/guidelines/python.md, testing.md and public-api.md |
agents |
gitlab |
.gitlab/merge_request_templates/Default.md, .gitlab/issue_templates/Bug.md and Feature.md |
— |
How they fit together:
- Shared instructions, local facts. The shared files never name a tracker, a branch pattern or a build command. They tell agents to read
.agents/project.md, where each team writes those facts. The profile ships as a skeleton: once a team edits it, it belongs to the repository. Until then,AGENTS.mdhas agents find the facts in the repository (build files, CI,CONTRIBUTING.md, git), say where they found them and offer to fill the profile, so no repository depends on an empty file. - One source for every agent. Codex, Cursor, Copilot and Claude Code read
AGENTS.md;CLAUDE.mdimports it for Claude Code. The review checklist lives once in.agents/prompts/review.md, and the Claude skill and subagent follow it. The prompt splits a large change into slices, reviewed in parallel where the agent can run sub-agents and one after another where it cannot, then looks at the change as a whole. - Skills with side effects wait to be asked.
/new-branch,/commit,/pr,/spec,/app-startand/app-stoprun only when you invoke them. Skill names avoid Claude Code's built-in commands, such as/reviewand/branch. - No hooks, no extra permissions.
.claude/settings.jsonholds deny rules only.Read(!.env.example)keeps.env.examplereadable: in adenylist, a rule that starts with!carves its paths out of the rules listed before it in the same file (Claude Code's permission rules). - Guidelines stay out of
docs/. Many projects publishdocs/as their documentation site, and the guidelines are for the people and agents who change the code, not for its users: they live in.agents/guidelines/, next to the profile and the prompts. python-serviceandpython-libraryboth ship.agents/guidelines/python.mdandtesting.md, with different content: give a repository one of them.
GitHub reads pull request and issue templates, CODE_OF_CONDUCT.md and SECURITY.md from your organisation's .github repository when a repository has none, so there is no GitHub pack. Add one if those files must live in every repository.
- Layout. A pack is a directory in
packs/, laid out like the target repository:packs/agents/AGENTS.mdbecomesAGENTS.md. Every file in it is delivered, so describe a pack inhub.yml, not in a README inside it. - Order. Packs apply in order; if two ship the same path, the later one wins. Declare dependencies under
packs:inhub.ymlwithrequires, and touchmark orders the packs for you. - Renames. When you rename a pack, list its old name under
formerlyinhub.yml, so files the old name shipped stay managed. - Size. Every file must be at least 64 bytes: smaller content can't prove where it came from.
- Reserved file. Don't ship
.engineering-assets.yml. It belongs to each repository. - Seed files. A file that a team is expected to fill in, like
.agents/project.md, can ship as a skeleton. Once edited, it belongs to the repository and is never touched again. - Project facts. Keep tracker keys, branch patterns, the pull request language and build commands out of shared files. They belong in the project profile, which the shared instructions tell agents to read.
- Commit first. touchmark ships what is committed on the default branch, not your working tree.
| To… | Do this |
|---|---|
| change a shared file everywhere | edit it in the pack, open a pull request in the hub, check plan, merge |
| let one repository keep its own version | edit the file in that repository (it becomes local), or add it to ignore |
| bring a diverged file back under the hub | run touchmark apply --adopt <path> in that repository (add --assume-opt-in when the hub subscribes it through an org: or group: entry and it has no opt-in file) and open a pull request there |
| retire a file | delete it from the pack; repositories that never changed it get a pull request deleting it |
| add a repository | add it to targets.yml; the next run visits it once it has opted in, or at once with opt_in: assumed |
| undo a closed sync pull request | reopen it, or tick Propose this content again in its description |
| resume a sync branch someone pushed to | tick Rebuild this branch in the pull request, or add a recreate entry with the branch head to .touchmark/operations.yml |
| move a target to another provider on the same host | add the old write account to known_authors in hub.yml |
| check the write account | read the weekly doctor job, or run the workflow by hand |
| upgrade touchmark | read the release notes, then merge the Dependabot or Renovate pull request. Dependabot's plan gets no secrets and shows the hub's side only; Renovate opens its merge request once you approve it on the Dependency Dashboard, and its plan checks the targets |
scripts/validate.sh runs what the CI runs, before you push:
bash scripts/validate.sh # touchmark check, actionlint on .github and .gitea workflows, the Bitbucket image pin
bash scripts/validate.sh --release # also fail unless touchmark is pinned to a published releaseIt checks a fresh copy of your working tree, committed or not, as a hub created from it would look. It needs touchmark on your PATH, or the command that runs it in TOUCHMARK, and actionlint or Docker. The GitLab CI file is checked by GitLab itself: use CI Lint in the project; bitbucket-pipelines.yml by Bitbucket's editor. --release fails while bitbucket-pipelines.yml still waits for that release.
A hub can open pull requests in every repository it targets, so treat it as the root of your supply chain:
- A merge into the hub runs code in your targets' CI: a sync pull request runs the target's workflows with its secrets before anyone reviews it. Protect the default branch, require review, and keep CODEOWNERS on
packs/,hub.yml,targets.yml,.touchmark/and the CI files. - Keep the write account on the default branch only: the
touchmark-distributeenvironment on GitHub, a protected, environment-scoped variable on GitLab, a deployment variable oftouchmark-distributerestricted to the default branch and to admins or selected users on Bitbucket Premium. The probe checks that no other job can see the key, anddistributerefuses to run otherwise, unlesshub.ymlsetssecurity.write_isolationtoexternal, or tononewith a reason. A private hub on GitHub Free can't pass: make the hub public, use the Team plan, or release the key from an external secret store through OIDC. - The hub's maintainers hold the write account on every platform: they can change the variables, the environments and the workflows. Choose them accordingly.
- One-off operations that override a safeguard live in
.touchmark/operations.ymland go through review. The workflows take no inputs, and touchmark refuses those flags in CI. - The read account can read every target, and anyone who can push a branch to the hub can use it. Keep hub write access tight.
- touchmark is pinned, and upgraded through review: the Action by commit, and the image by digest on GitLab, Gitea, Forgejo and Bitbucket. The Action runs the image of its own release, by digest, and only after
gh attestation verifyhas shown that touchmark's publish workflow built it. Dependabot's pull requests get no secrets, so theirplanchecks no target: it shows the hub's side, warns, and passes. Read the release notes, and runplanwith the read key yourself before you merge. Renovate waits a week for other updates, but GHCR gives no release dates, so for the touchmark image it waits for your approval on its Dependency Dashboard instead. - Read the ⚠ section of every sync pull request. It lists changes to workflows, CI configuration, agent settings, skills and subagents, and CODEOWNERS.
- In a public repository, GitHub disables scheduled workflows after 60 days without activity. Push to the hub or re-enable the workflow if the daily run stops. On GitLab, a schedule stops when its owner loses access: give it to an account that stays.
- On Bitbucket, anyone with write access to the hub can run its custom pipelines and pass them variables, which override deployment variables. The template's scripts reference none, so the website asks for none, but the API takes any. touchmark's git ignores the variables that would change its config or the programs it runs (
GIT_CONFIG_*,GIT_SSH_COMMAND,LD_PRELOAD, …), yet the rest of the run's environment is the caller's: on Premium, restricttouchmark-distributeto admins or selected users, so that deployment permissions pause anyone else's run. - A public hub skips private targets and prints only how many, because anyone can read its CI logs.
touchmark never merges. It never overwrites or deletes a file a repository has changed.
The template and its starter packs are released under MIT-0: use and change them without attribution. touchmark itself is Apache-2.0.