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
63 changes: 63 additions & 0 deletions .github/actions/signed-push/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!-- SPDX-License-Identifier: MPL-2.0 -->
# `signed-push` — verified commits from CI/AI tooling

Estate composite action that pushes a branch's local commits as GitHub **Verified**
commits, so they satisfy `required_signatures` branch rulesets.

## Why

Commits made by automation with a **PAT** or the default `GITHUB_TOKEN` show as
**Unverified**, and any branch whose ruleset requires signed commits then **cannot
merge** (e.g. `mergeStateStatus: BLOCKED`). The only reliable way for automation to
produce Verified commits is the GraphQL [`createCommitOnBranch`] mutation
authenticated as a **GitHub App installation** — GitHub signs those commits itself.

This action mints an App installation token and delegates the push to
[`Asana/push-signed-commits`], which diffs local vs. remote and replays the local
commits through `createCommitOnBranch`.

## Prerequisites (one-time, account owner)

1. Register a **GitHub App** on the org (Settings → Developer settings → GitHub Apps).
Repository permissions: **Contents: Read & write**, **Workflows: Read & write**
(to edit `.github/workflows`), optionally **Administration: Read & write** (for
`delete_repo`).
2. **Install** the App on the org/repos and **generate a private key** (`.pem`).
3. Store **`APP_ID`** and **`APP_PRIVATE_KEY`** as org (or repo) secrets.
4. If the branch ruleset also enforces **pull-request-only merges** or a **merge
queue**, add the App to that ruleset's **bypass list** — `required_signatures`
is satisfied by this action, but those are separate rules.

## Usage

```yaml
jobs:
update:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Make changes
run: |
echo "generated" >> CHANGELOG.md
git config user.name "estate-bot[bot]"
git config user.email "estate-bot[bot]@users.noreply.github.com"
git commit -am "chore: regenerate changelog"
- name: Push as Verified
uses: hyperpolymath/standards/.github/actions/signed-push@main
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# branch: defaults to the current ref
```

## Notes

- Pinned to SHAs (`create-github-app-token` v3.2.0, `push-signed-commits` v1) per
estate supply-chain policy.
- Run **after** local commits exist in the job — the action pushes the delta
between your local branch and its remote.

[`createCommitOnBranch`]: https://github.blog/changelog/2021-09-13-a-simpler-api-for-authoring-commits/
[`Asana/push-signed-commits`]: https://github.com/Asana/push-signed-commits
46 changes: 46 additions & 0 deletions .github/actions/signed-push/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Signed push (verified commits via GitHub App)
description: >
Re-push the current branch's local commits as GitHub-"Verified" commits using a
GitHub App installation token and the createCommitOnBranch GraphQL mutation (via
Asana/push-signed-commits). This satisfies `required_signatures` branch rulesets
that a PAT or the default GITHUB_TOKEN cannot. Run this AFTER you have made local
commits in the job (checkout -> edit -> git commit -> this action).

inputs:
app-id:
description: GitHub App ID (pass from a repository/organization variable or secret).
required: true
private-key:
description: GitHub App private key, PEM contents (pass from a secret).
required: true
branch:
description: Branch to push as verified (local name == remote name). Defaults to the current ref.
required: false
default: ''

runs:
using: composite
steps:
- name: Mint GitHub App installation token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}

- name: Resolve branch name
id: branch
shell: bash
env:
BRANCH_INPUT: ${{ inputs.branch }}
run: echo "name=${BRANCH_INPUT:-$GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"

# createCommitOnBranch extracts authorship from the App installation credential
# and auto-marks the resulting commits as "Verified". A PAT would leave them
# Unverified — which is why this must be an App token, not GITHUB_TOKEN/PAT.
- name: Push local commits as Verified
uses: Asana/push-signed-commits@6e073ae68b04d4322be613a6cabf391acb03ca44 # v1
with:
github-token: ${{ steps.app-token.outputs.token }}
local_branch_name: ${{ steps.branch.outputs.name }}
remote_branch_name: ${{ steps.branch.outputs.name }}
Loading