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
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ looks tidier for 1:1 but forks the `github-release` download and breaks its verb
`build-release-task.yml`. Per the per-target subsetting rule above, delete the target jobs not
shipped and add the ones that are. `build-release-task.yml`'s `github-release` job is untouched, but the file
is not byte-identical because its `needs`/job list reflects the repo's own targets. Making that
list itself target-agnostic is a larger "factor build from orchestration" refactor that is
intentionally not done.
list itself target-agnostic is the release-chain phase of `docs/reusable-workflows.md` in the
hub, where the orchestrator becomes a hub-hosted task and each target a composite-action hook,
and until that phase ships the list stays per repo.

## Map your outputs to the right seam

Expand Down
1 change: 1 addition & 0 deletions .agents/skills/workflow-ci-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ description: >-
- **Outcomes, not bytes.** A workflow is correct when it satisfies the section 4 contract against the expected inputs and outputs, not when it matches a catalog snippet byte for byte. Two repos may implement one guarantee with different YAML.
- **Applicability.** A guarantee governing a construct the repo does not contain is N/A: recorded, excluded from the verdict, never a defect. A source-only pipeline is mostly N/A and that is fine.
- **Operational is binary.** Every applicable guarantee holds, or the workflow is not operational. A single applicable input-output mismatch is a defect regardless of how clean the YAML looks.
- **Reached, not carried.** A standard workflow whose job graph is identical across repos of a type is a `workflow_call` task the hub hosts once, and a repo carries only a caller stub pinned to a hub release commit plus a composite-action hook at `.github/actions/<hook>` for what is its own. The merge-bot is the first, and `docs/reusable-workflows.md` in the hub carries the model, the hook contract, and the phase each workflow migrates in. Until a workflow's phase ships, its copy is graded as below.
- **Two layers.** Orchestration (the PR entry workflow, publisher, version/release/badge jobs) is generic and standard at the job level. Build leaves (`build-<target>-task.yml`) are repo-owned. Inputs like `github`/`nuget`/`dockerhub`/`expect_release_assets` live on the orchestrator, a leaf only receives `ref`/`branch`/`smoke` and a derived `push`, so assert each input in the layer that declares it. What a repo curates is the list of targets, and adding or dropping one edits the whole surface together: the `enable_<target>` input, the `build-<target>` job and its `github-release` `needs:` entry, the `changes` paths-filter entry and output, and the `smoke-build` enable-forward (D6.4).

## Style Rules That Break in One-Line Diffs
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/fleet-skills/.source-digest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b1c02e77cc574d26
c69f21b41228327e
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ looks tidier for 1:1 but forks the `github-release` download and breaks its verb
`build-release-task.yml`. Per the per-target subsetting rule above, delete the target jobs not
shipped and add the ones that are. `build-release-task.yml`'s `github-release` job is untouched, but the file
is not byte-identical because its `needs`/job list reflects the repo's own targets. Making that
list itself target-agnostic is a larger "factor build from orchestration" refactor that is
intentionally not done.
list itself target-agnostic is the release-chain phase of `docs/reusable-workflows.md` in the
hub, where the orchestrator becomes a hub-hosted task and each target a composite-action hook,
and until that phase ships the list stays per repo.

## Map your outputs to the right seam

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ description: >-
- **Outcomes, not bytes.** A workflow is correct when it satisfies the section 4 contract against the expected inputs and outputs, not when it matches a catalog snippet byte for byte. Two repos may implement one guarantee with different YAML.
- **Applicability.** A guarantee governing a construct the repo does not contain is N/A: recorded, excluded from the verdict, never a defect. A source-only pipeline is mostly N/A and that is fine.
- **Operational is binary.** Every applicable guarantee holds, or the workflow is not operational. A single applicable input-output mismatch is a defect regardless of how clean the YAML looks.
- **Reached, not carried.** A standard workflow whose job graph is identical across repos of a type is a `workflow_call` task the hub hosts once, and a repo carries only a caller stub pinned to a hub release commit plus a composite-action hook at `.github/actions/<hook>` for what is its own. The merge-bot is the first, and `docs/reusable-workflows.md` in the hub carries the model, the hook contract, and the phase each workflow migrates in. Until a workflow's phase ships, its copy is graded as below.
- **Two layers.** Orchestration (the PR entry workflow, publisher, version/release/badge jobs) is generic and standard at the job level. Build leaves (`build-<target>-task.yml`) are repo-owned. Inputs like `github`/`nuget`/`dockerhub`/`expect_release_assets` live on the orchestrator, a leaf only receives `ref`/`branch`/`smoke` and a derived `push`, so assert each input in the layer that declares it. What a repo curates is the list of targets, and adding or dropping one edits the whole surface together: the `enable_<target>` input, the `build-<target>` job and its `github-release` `needs:` entry, the `changes` paths-filter entry and output, and the `smoke-build` enable-forward (D6.4).

## Style Rules That Break in One-Line Diffs
Expand Down
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ end_of_line = lf
# Python is CRLF by the `[*]` default, since universal newlines accept it and it is commonly edited on Windows.
# Pin LF only for a `.py` executed directly via its shebang, by path.
# Here that is the CI validation entry point and the fleet-audit runner.
[spec/{validate,audit,fidelity_honesty}.py]
[spec/{validate,audit,fidelity_honesty,workflow_reuse}.py]
end_of_line = lf

# The agent-safety kit's Python is shebang-executable tooling run by path, namely the PreToolUse hook and its installer.
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ catalog/snippets/husky/pre-commit text eol=lf
spec/validate.py text eol=lf
spec/audit.py text eol=lf
spec/fidelity_honesty.py text eol=lf
spec/workflow_reuse.py text eol=lf
host-setup/agent-safety/gh-write-guard.py text eol=lf
host-setup/agent-safety/install.py text eol=lf
host-setup/agent-safety/test_install.py text eol=lf
Expand Down
5 changes: 5 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,11 @@ After the final push, sweep-resolve stale older threads for removed code paths.
- **Proved against** - the paragraph following the agent snippet in `STANDUP.md` section 0 on `develop` at `676a2bd`, unchanged since `77be3a3`.
- **Delete when** - the sentence is reworded for any reason, since the entry is about this phrasing rather than about the rule it states.

- **A hyphenated key such as `inputs.app-login` or `inputs.delete-branch` cannot be read with dot notation in a GitHub Actions expression, since `-` parses as subtraction, so the merge-bot task's `if:` conditions never match and its `env:` values never resolve.** Raised as four suppressed findings against `.github/workflows/merge-bot-task.yml`, each a variant of the one claim.
- **Disproved by** - reading the expressions reference, which states that property dereference syntax needs a name that starts with a letter or `_` and contains only alphanumeric characters, `-`, or `_`, so a hyphen is inside the allowed set and index syntax is required only for a name outside it. And by the same file, whose every job reads `steps.app-token.outputs.token` with dot notation on the same hyphenated shape, the expression the fleet's merge-bot has resolved on every Dependabot merge it has performed. `actionlint` parses the file without a finding.
- **Proved against** - `.github/workflows/merge-bot-task.yml` on `feature/reusable-workflows` at `210d88f`, and the "About contexts" property dereference rule in the GitHub Actions contexts reference read on 2026-08-15.
- **Delete when** - the task stops declaring hyphenated inputs, or the expressions reference changes the allowed set.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) to find the section that governs your change, and [GOVERNANCE.md](../GOVERNANCE.md) for the rule text itself. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) (its General section plus the relevant language section) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions, and keep those focused on the change itself.
Expand Down
173 changes: 11 additions & 162 deletions .github/workflows/merge-bot-pull-request.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
name: Merge bot pull request action

# Auto-merges in-repo bot PRs (Dependabot, codegen): enable on opened/reopened, disable on a maintainer push.
# - Merge method by base: develop = squash, main = merge.
# - App token, not GITHUB_TOKEN: fires downstream workflows on merge, and grants write on read-only Dependabot PRs.
# - pull_request_target rather than pull_request, since jobs hold the App key, so the workflow and action SHAs resolve from the trusted base rather than the PR head.
# This is safe because no job checks out PR code, each one running gh pr merge by URL.
# Thin caller: the merge-bot is the hub's reusable merge-bot-task.yml, which every fleet repo reaches rather than carries.
# The trigger is pull_request_target so the called workflow resolves from the trusted base rather than the PR head, and no job checks out PR code.
on:
pull_request_target:
types: [opened, reopened, synchronize]
Expand All @@ -15,162 +12,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:

merge-dependabot:
name: Merge dependabot pull request job
runs-on: ubuntu-latest
# Dependabot PRs from this repo rather than from forks.
# Only on opened or reopened, so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: write
pull-requests: write

steps:

- name: Generate GitHub App token step
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}

# Auto-merge every tier, semver-major included: the required checks are the gate, not the bump magnitude.
- name: Merge pull request step
run: |
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
*)
echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"
exit 1
;;
esac
gh pr merge --auto "$method" "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

merge-codegen:
name: Merge codegen pull request job
runs-on: ubuntu-latest
# Codegen PRs from this repo.
# Head and base pairing is enforced strictly, codegen-main to main and codegen-develop to develop.
# Only on opened or reopened, so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'ptr727-codegen[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository &&
(
(github.event.pull_request.head.ref == 'codegen-main' && github.event.pull_request.base.ref == 'main') ||
(github.event.pull_request.head.ref == 'codegen-develop' && github.event.pull_request.base.ref == 'develop')
)
permissions:
contents: write
pull-requests: write

steps:

- name: Generate GitHub App token step
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}
# Every write in the called workflow uses the App token, so GITHUB_TOKEN gets no scope.
permissions: {}

- name: Merge pull request step
run: |
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
*)
echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"
exit 1
;;
esac
gh pr merge --auto "$method" "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

merge-upstream-version:
name: Merge upstream version pull request job
runs-on: ubuntu-latest
# Upstream-version bump PRs from the App.
# Head and base pairing is enforced, upstream-version-main to main and upstream-version-develop to develop.
# Only on opened or reopened, so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'ptr727-codegen[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository &&
(
(github.event.pull_request.head.ref == 'upstream-version-main' && github.event.pull_request.base.ref == 'main') ||
(github.event.pull_request.head.ref == 'upstream-version-develop' && github.event.pull_request.base.ref == 'develop')
)
permissions:
contents: write
pull-requests: write

steps:

- name: Generate GitHub App token step
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}

- name: Merge pull request step
run: |
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
*)
echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"
exit 1
;;
esac
gh pr merge --auto "$method" "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

disable-auto-merge-on-maintainer-push:
name: Disable auto-merge on maintainer push job
runs-on: ubuntu-latest
# Fires when a maintainer pushes to a bot's branch, meaning a synchronize whose actor is not the bot.
# It disables auto-merge so the maintainer's commits do not merge with the bot's, and the maintainer re-enables it manually.
# The disable call is idempotent.
if: >-
github.event.action == 'synchronize' &&
github.event.pull_request.head.repo.full_name == github.repository &&
(
github.event.pull_request.user.login == 'dependabot[bot]' ||
github.event.pull_request.user.login == 'ptr727-codegen[bot]'
) &&
github.actor != github.event.pull_request.user.login
permissions:
pull-requests: write

steps:

- name: Generate GitHub App token step
# App token because a Dependabot PR's GITHUB_TOKEN is read-only regardless of who triggered the event.
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}
jobs:

- name: Disable auto-merge step
run: gh pr merge --disable-auto "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
merge-bot:
name: Merge bot pull request job
uses: ./.github/workflows/merge-bot-task.yml
secrets:
CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}
Loading