Skip to content
Open
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
21 changes: 20 additions & 1 deletion actions/signing-event/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ inputs:
token:
description: 'GitHub token'
required: true
base-branch:
description: 'Base branch name (defaults to auto-detected default branch or "main")'
required: false
default: ''

runs:
using: "composite"
Expand All @@ -30,6 +34,20 @@ runs:
fetch-depth: 0
persist-credentials: true

- id: detect-base-branch
env:
INPUT_BASE_BRANCH: ${{ inputs.base-branch }}
run: |
if [ -n "$INPUT_BASE_BRANCH" ]; then
echo "base-branch=$INPUT_BASE_BRANCH" >> $GITHUB_OUTPUT
else
# Try to auto-detect default branch
BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
echo "base-branch=$BASE_BRANCH" >> $GITHUB_OUTPUT
echo "Auto-detected base branch: $BASE_BRANCH"
fi
shell: bash

- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.14"
Expand Down Expand Up @@ -70,6 +88,7 @@ runs:
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
STATUS: ${{ steps.status.outputs.status }}
BASE_BRANCH: ${{ steps.detect-base-branch.outputs.base-branch }}
with:
github-token: ${{ inputs.token }}
script: |
Expand All @@ -92,7 +111,7 @@ runs:
body: `Processing signing event ${process.env.GITHUB_REF_NAME}, please wait.`,
draft: true,
head: process.env.GITHUB_REF_NAME,
base: "main",
base: process.env.BASE_BRANCH,
Copy link
Member

@jku jku Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we decide to avoid configuration and just use the default branch, we can use
something like base: context.payload.repository.default_branch here

(I think that's the correct path but the documentation is confusing to me so that definitely requires testing)

})
pr = response.data.number
console.log(`Created pull request #${pr}`)
Expand Down
4 changes: 4 additions & 0 deletions docs/SIGNER-SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@ pull-remote = origin
# push-remote: If you are allowed to push to the TUF repository, you can use the same value
# as pull-remote. Otherwise use the remote name of your fork
push-remote = origin

# base-branch: The base branch name (optional)
# If not provided, tuf-on-ci-sign will auto-detect the default branch or use 'main'
# base-branch = main
```
45 changes: 45 additions & 0 deletions repo/tuf_on_ci/_git_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2023 Google LLC

"""Shared git utilities for TUF-on-CI"""

import logging
import subprocess

logger = logging.getLogger(__name__)


def _git(cmd: list[str]) -> subprocess.CompletedProcess:
"""Execute a git command with TUF-on-CI user configuration"""
cmd = [
"git",
"-c",
"user.name=TUF-on-CI",
"-c",
"user.email=41898282+github-actions[bot]@users.noreply.github.com",
*cmd,
]
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
logger.debug("%s:\n%s", cmd, proc.stdout)
return proc


def _get_base_branch(remote: str = "origin") -> str:
"""Get base branch by auto-detecting from git remote

Args:
remote: The git remote name (defaults to "origin")

Returns:
The name of the base branch
"""
try:
# Try to auto-detect default branch from git remote
result = _git(["symbolic-ref", f"refs/remotes/{remote}/HEAD"])
# Output is like 'refs/remotes/origin/main'
ref = result.stdout.strip()
branch = ref.split("/")[-1]
logger.debug("Auto-detected base branch: %s", branch)
return branch
except (subprocess.CalledProcessError, IndexError):
logger.debug("Failed to auto-detect base branch, defaulting to 'main'")
return "main"
16 changes: 1 addition & 15 deletions repo/tuf_on_ci/build_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,19 @@

import logging
import os
import subprocess
from datetime import UTC, datetime, timedelta
from urllib import parse

import click
from tuf.api.metadata import Root, Signed, Targets

from tuf_on_ci._git_utils import _git
from tuf_on_ci._repository import CIRepository
from tuf_on_ci._version import __version__

logger = logging.getLogger(__name__)


def _git(cmd: list[str]) -> subprocess.CompletedProcess:
cmd = [
"git",
"-c",
"user.name=tuf-on-ci",
"-c",
"user.email=41898282+github-actions[bot]@users.noreply.github.com",
*cmd,
]
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
logger.debug("%s:\n%s", cmd, proc.stdout)
return proc


def build_description(repo: CIRepository) -> str:
lines = [
"## TUF Repository state",
Expand Down
15 changes: 1 addition & 14 deletions repo/tuf_on_ci/create_signing_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,12 @@

import click

from tuf_on_ci._git_utils import _git
from tuf_on_ci._repository import CIRepository

logger = logging.getLogger(__name__)


def _git(cmd: list[str]) -> subprocess.CompletedProcess:
cmd = [
"git",
"-c",
"user.name=TUF-on-CI",
"-c",
"user.email=41898282+github-actions[bot]@users.noreply.github.com",
*cmd,
]
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
logger.debug("%s:\n%s", cmd, proc.stdout)
return proc


@click.command() # type: ignore[arg-type]
@click.option("-v", "--verbose", count=True, default=0)
@click.option("--push/--no-push", default=False)
Expand Down
16 changes: 1 addition & 15 deletions repo/tuf_on_ci/online_sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,15 @@
"""Command line online signing tool for TUF-on-CI"""

import logging
import subprocess

import click

from tuf_on_ci._git_utils import _git
from tuf_on_ci._repository import CIRepository

logger = logging.getLogger(__name__)


def _git(cmd: list[str]) -> subprocess.CompletedProcess:
cmd = [
"git",
"-c",
"user.name=tuf-on-ci",
"-c",
"user.email=41898282+github-actions[bot]@users.noreply.github.com",
*cmd,
]
proc = subprocess.run(cmd, check=True, text=True)
logger.debug("%s:\n%s", cmd, proc.stdout)
return proc


@click.command() # type: ignore[arg-type]
@click.option("-v", "--verbose", count=True, default=0)
@click.option("--push/--no-push", default=False)
Expand Down
21 changes: 5 additions & 16 deletions repo/tuf_on_ci/signing_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,12 @@

import click

from tuf_on_ci._git_utils import _get_base_branch, _git
from tuf_on_ci._repository import CIRepository

logger = logging.getLogger(__name__)


def _git(cmd: list[str]) -> subprocess.CompletedProcess:
cmd = [
"git",
"-c",
"user.name=TUF-on-CI",
"-c",
"user.email=41898282+github-actions[bot]@users.noreply.github.com",
*cmd,
]
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
logger.debug("%s:\n%s", cmd, proc.stdout)
return proc


def _find_changed_roles(known_good_dir: str, signing_event_dir: str) -> set[str]:
# find the files that have changed or been added
# TODO what about removed roles?
Expand Down Expand Up @@ -173,7 +160,8 @@ def update_targets(verbose: int, push: bool) -> None:
sys.exit(1)

# Find the known-good commit
merge_base = _git(["merge-base", "origin/main", "HEAD"]).stdout.strip()
base_branch = _get_base_branch()
merge_base = _git(["merge-base", f"origin/{base_branch}", "HEAD"]).stdout.strip()
if head == merge_base:
click.echo("This signing event contains no changes yet")
sys.exit(1)
Expand Down Expand Up @@ -306,7 +294,8 @@ def status(verbose: int) -> None:
sys.exit(1)

# Find the known-good commit
merge_base = _git(["merge-base", "origin/main", "HEAD"]).stdout.strip()
base_branch = _get_base_branch()
merge_base = _git(["merge-base", f"origin/{base_branch}", "HEAD"]).stdout.strip()
if head == merge_base:
click.echo("This signing event contains no changes yet")
sys.exit(1)
Expand Down
3 changes: 3 additions & 0 deletions signer/create-config-file.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@ user-name = @${GITHUB_HANDLE}
# Git remotes
pull-remote = origin
push-remote = origin

# Base branch name (optional, defaults to auto-detected default branch or 'main')
# base-branch = main
EOF
7 changes: 4 additions & 3 deletions signer/tuf_on_ci_sign/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ def signing_event(name: str, config: User) -> Generator[SignerRepository, None,
try:
git(["checkout", f"{config.pull_remote}/{name}"])
except subprocess.CalledProcessError:
click.echo("Remote branch not found: branching off from main")
git_expect(["checkout", f"{config.pull_remote}/main"])
click.echo(f"Remote branch not found: branching off from {config.base_branch}")
git_expect(["checkout", f"{config.pull_remote}/{config.base_branch}"])

try:
# checkout the base of this signing event in another directory
with TemporaryDirectory() as temp_dir:
base_sha = git_expect(["merge-base", f"{config.pull_remote}/main", "HEAD"])
base_ref = f"{config.pull_remote}/{config.base_branch}"
base_sha = git_expect(["merge-base", base_ref, "HEAD"])
event_sha = git_expect(["rev-parse", "HEAD"])
git_expect(["clone", "--quiet", toplevel, temp_dir])
git_expect(["-C", temp_dir, "checkout", "--quiet", base_sha])
Expand Down
46 changes: 46 additions & 0 deletions signer/tuf_on_ci_sign/_user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
import platform
import subprocess
import sys
from configparser import ConfigParser

Expand All @@ -25,6 +26,37 @@ def bold(text: str) -> str:
return click.style(text, bold=True)


def _get_base_branch_internal(remote: str = "origin") -> str:
"""Get base branch by auto-detecting from git remote

Args:
remote: The git remote name (defaults to "origin")

Returns:
The name of the base branch
"""
try:
# Try to auto-detect default branch from git remote
cmd = [
"git",
"-c",
"user.name=TUF-on-CI",
"-c",
"user.email=41898282+github-actions[bot]@users.noreply.github.com",
"symbolic-ref",
f"refs/remotes/{remote}/HEAD",
]
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
# Output is like 'refs/remotes/origin/main'
ref = result.stdout.strip()
branch = ref.split("/")[-1]
logger.debug("Auto-detected base branch: %s", branch)
return branch
except (subprocess.CalledProcessError, IndexError):
logger.debug("Failed to auto-detect base branch, defaulting to 'main'")
return "main"


class User:
"""Class that manages user configuration and manages the users signer cache"""

Expand Down Expand Up @@ -69,6 +101,20 @@ def __init__(self, path: str):
# signer cache gets populated as they are used the first time
self._signers: dict[str, Signer] = {}

# detect or use configured base branch
self.base_branch = self._get_base_branch()

def _get_base_branch(self) -> str:
"""Get base branch from config or auto-detect"""
try:
# First try to get from config
return self._config["settings"]["base-branch"]
except KeyError:
pass

# Auto-detect using shared utility
return _get_base_branch_internal(remote=self.pull_remote)

def get_signer(self, key: Key) -> Signer:
"""Returns a Signer for the given public key

Expand Down
8 changes: 4 additions & 4 deletions signer/tuf_on_ci_sign/delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,12 @@ def verify_signers(response: str) -> list[str]:
return (config, online_key)


def _sigstore_import(pull_remote: str) -> Key:
def _sigstore_import(pull_remote: str, base_branch: str) -> Key:
# WORKAROUND: build sigstore key and uri here since there is no import yet
issuer = "https://token.actions.githubusercontent.com"
repo = get_repo_name(pull_remote)

id = f"https://github.com/{repo}/.github/workflows/online-sign.yml@refs/heads/main"
id = f"https://github.com/{repo}/.github/workflows/online-sign.yml@refs/heads/{base_branch}"
key = SigstoreKey(
"abcd", "sigstore-oidc", "Fulcio", {"issuer": issuer, "identity": id}
)
Expand Down Expand Up @@ -226,7 +226,7 @@ def _collect_online_key(user_config: User) -> Key:
show_default=True,
)
if choice == 1:
return _sigstore_import(user_config.pull_remote)
return _sigstore_import(user_config.pull_remote, user_config.base_branch)
if choice == 2:
key_id = _collect_string("Enter a Google Cloud KMS key id")
try:
Expand Down Expand Up @@ -310,7 +310,7 @@ def _init_repository(repo: SignerRepository) -> bool:
targets_config, _ = _get_offline_input("targets", deepcopy(root_config))

# As default we offer sigstore online key(s)
keys = _sigstore_import(repo.user.pull_remote)
keys = _sigstore_import(repo.user.pull_remote, repo.user.base_branch)
default_config = OnlineConfig(
keys, 2, 1, root_config.expiry_period, root_config.signing_period
)
Expand Down