diff --git a/actions/signing-event/action.yml b/actions/signing-event/action.yml index 5b84d06..886a526 100644 --- a/actions/signing-event/action.yml +++ b/actions/signing-event/action.yml @@ -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" @@ -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" @@ -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: | @@ -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, }) pr = response.data.number console.log(`Created pull request #${pr}`) diff --git a/docs/SIGNER-SETUP.md b/docs/SIGNER-SETUP.md index 9da6c9f..8a1dab9 100644 --- a/docs/SIGNER-SETUP.md +++ b/docs/SIGNER-SETUP.md @@ -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 ``` diff --git a/repo/tuf_on_ci/_git_utils.py b/repo/tuf_on_ci/_git_utils.py new file mode 100644 index 0000000..013ad9d --- /dev/null +++ b/repo/tuf_on_ci/_git_utils.py @@ -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" diff --git a/repo/tuf_on_ci/build_repository.py b/repo/tuf_on_ci/build_repository.py index e55b52a..22e2ae1 100644 --- a/repo/tuf_on_ci/build_repository.py +++ b/repo/tuf_on_ci/build_repository.py @@ -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", diff --git a/repo/tuf_on_ci/create_signing_events.py b/repo/tuf_on_ci/create_signing_events.py index 9858f23..e0ac5a6 100644 --- a/repo/tuf_on_ci/create_signing_events.py +++ b/repo/tuf_on_ci/create_signing_events.py @@ -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) diff --git a/repo/tuf_on_ci/online_sign.py b/repo/tuf_on_ci/online_sign.py index 95b9f3f..8653821 100644 --- a/repo/tuf_on_ci/online_sign.py +++ b/repo/tuf_on_ci/online_sign.py @@ -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) diff --git a/repo/tuf_on_ci/signing_event.py b/repo/tuf_on_ci/signing_event.py index 9e92531..21766fe 100644 --- a/repo/tuf_on_ci/signing_event.py +++ b/repo/tuf_on_ci/signing_event.py @@ -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? @@ -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) @@ -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) diff --git a/signer/create-config-file.sh b/signer/create-config-file.sh index 24c59e8..82e66b6 100755 --- a/signer/create-config-file.sh +++ b/signer/create-config-file.sh @@ -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 diff --git a/signer/tuf_on_ci_sign/_common.py b/signer/tuf_on_ci_sign/_common.py index 703fc61..ce931a9 100644 --- a/signer/tuf_on_ci_sign/_common.py +++ b/signer/tuf_on_ci_sign/_common.py @@ -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]) diff --git a/signer/tuf_on_ci_sign/_user.py b/signer/tuf_on_ci_sign/_user.py index cf16a32..2e5ec61 100644 --- a/signer/tuf_on_ci_sign/_user.py +++ b/signer/tuf_on_ci_sign/_user.py @@ -1,6 +1,7 @@ import logging import os import platform +import subprocess import sys from configparser import ConfigParser @@ -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""" @@ -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 diff --git a/signer/tuf_on_ci_sign/delegate.py b/signer/tuf_on_ci_sign/delegate.py index cd2fe56..773a419 100755 --- a/signer/tuf_on_ci_sign/delegate.py +++ b/signer/tuf_on_ci_sign/delegate.py @@ -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} ) @@ -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: @@ -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 )