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
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
labels:
- "security"
- "dependencies"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "security"
- "dependencies"
77 changes: 77 additions & 0 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Dependabot auto-merge workflow
#
# Requires repository secrets:
# APP_ID — GitHub App ID with contents:write and pull-requests:write
# APP_PRIVATE_KEY — GitHub App private key
#
# Auto-approves and enables auto-merge for Dependabot PRs that are:
# - GitHub Actions updates (patch or minor version bumps)
# - Security updates for any ecosystem (patch or minor)
# - Indirect (transitive) dependency updates
# Major version updates are always left for human review.
# Uses --auto so the merge waits for all required CI checks to pass.
#
# Safety model: application ecosystems use open-pull-requests-limit: 0 in
# dependabot.yml, so the only app-ecosystem PRs Dependabot can create are
# security updates. This workflow adds defense-in-depth by also checking
# the package ecosystem.
name: Dependabot auto-merge

on:
pull_request_target:
branches:
- main

permissions: {}

jobs:
dependabot:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v2
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"

- name: Determine if auto-merge eligible
id: eligible
run: |
UPDATE_TYPE="${{ steps.metadata.outputs.update-type }}"
DEP_TYPE="${{ steps.metadata.outputs.dependency-type }}"
ECOSYSTEM="${{ steps.metadata.outputs.package-ecosystem }}"

# Must be patch, minor, or indirect
if [[ "$UPDATE_TYPE" != "version-update:semver-patch" && \
"$UPDATE_TYPE" != "version-update:semver-minor" && \
"$DEP_TYPE" != "indirect" ]]; then
echo "eligible=false" >> "$GITHUB_OUTPUT"
echo "Skipping: major update requires human review"
exit 0
fi

# GitHub Actions version updates are always eligible
# App ecosystem PRs can only exist as security updates (limit: 0)
echo "eligible=true" >> "$GITHUB_OUTPUT"
echo "Auto-merge eligible: ecosystem=$ECOSYSTEM update=$UPDATE_TYPE"

- name: Generate app token
if: steps.eligible.outputs.eligible == 'true'
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Approve and enable auto-merge
if: steps.eligible.outputs.eligible == 'true'
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
217 changes: 217 additions & 0 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# Dependency vulnerability audit
#
# Auto-detects ecosystems present in the repository and runs the appropriate
# audit tool. Fails the build if any dependency has a known security advisory.
#
# Add "dependency-audit" as a required status check in branch protection.
#
# Pinned tool versions (update deliberately):
# govulncheck v1.1.4 | cargo-audit 0.22.1 | pip-audit 2.9.0
name: Dependency audit

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

jobs:
detect:
name: Detect ecosystems
runs-on: ubuntu-latest
outputs:
npm: ${{ steps.check.outputs.npm }}
pnpm: ${{ steps.check.outputs.pnpm }}
gomod: ${{ steps.check.outputs.gomod }}
cargo: ${{ steps.check.outputs.cargo }}
pip: ${{ steps.check.outputs.pip }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: Detect package ecosystems
id: check
run: |
# npm — look for package-lock.json anywhere (excluding node_modules)
if find . -name 'package-lock.json' -not -path '*/node_modules/*' | grep -q .; then
echo "npm=true" >> "$GITHUB_OUTPUT"
else
echo "npm=false" >> "$GITHUB_OUTPUT"
fi

# pnpm — look for pnpm-lock.yaml anywhere
if find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' | grep -q .; then
echo "pnpm=true" >> "$GITHUB_OUTPUT"
else
echo "pnpm=false" >> "$GITHUB_OUTPUT"
fi

# Go modules — detect via go.mod (not go.sum, which may not exist)
if find . -name 'go.mod' -not -path '*/vendor/*' | grep -q .; then
echo "gomod=true" >> "$GITHUB_OUTPUT"
else
echo "gomod=false" >> "$GITHUB_OUTPUT"
fi

# Cargo — detect via Cargo.toml anywhere (lockfile may not exist for libraries)
if find . -name 'Cargo.toml' -not -path '*/target/*' | grep -q .; then
echo "cargo=true" >> "$GITHUB_OUTPUT"
else
echo "cargo=false" >> "$GITHUB_OUTPUT"
fi

# Python — detect pyproject.toml or requirements.txt anywhere
if find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q . || \
find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q .; then
echo "pip=true" >> "$GITHUB_OUTPUT"
else
echo "pip=false" >> "$GITHUB_OUTPUT"
fi

audit-npm:
name: npm audit
needs: detect
if: needs.detect.outputs.npm == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "lts/*"

- name: Audit npm dependencies
run: |
# Audit each package-lock.json found in the repo
status=0
while IFS= read -r dir; do
echo "::group::npm audit $dir"
if ! (cd "$dir" && npm audit --audit-level=low); then
status=1
fi
echo "::endgroup::"
done < <(find . -name 'package-lock.json' -not -path '*/node_modules/*' -exec dirname {} \;)
exit $status

audit-pnpm:
name: pnpm audit
needs: detect
if: needs.detect.outputs.pnpm == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "lts/*"
cache: "pnpm"

- name: Audit pnpm dependencies
run: |
# Audit each pnpm-lock.yaml found in the repo
status=0
while IFS= read -r dir; do
echo "::group::pnpm audit $dir"
if ! (cd "$dir" && pnpm audit --audit-level low); then
status=1
fi
echo "::endgroup::"
done < <(find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' -exec dirname {} \;)
exit $status

audit-go:
name: govulncheck
needs: detect
if: needs.detect.outputs.gomod == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
with:
go-version: "stable"

- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4

- name: Audit Go dependencies
run: |
status=0
while IFS= read -r dir; do
echo "::group::govulncheck $dir"
if ! (cd "$dir" && govulncheck ./...); then
status=1
fi
echo "::endgroup::"
done < <(find . -name 'go.mod' -not -path '*/vendor/*' -exec dirname {} \;)
exit $status

audit-cargo:
name: cargo audit
needs: detect
if: needs.detect.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- uses: dtolnay/rust-toolchain@stable

- name: Install cargo-audit
run: cargo install cargo-audit@0.22.1 --locked

- name: Audit Cargo dependencies
run: |
# cargo audit operates on Cargo.lock at workspace root
# For workspaces, a single audit at root covers all crates
status=0
while IFS= read -r dir; do
echo "::group::cargo audit $dir"
if ! (cd "$dir" && cargo generate-lockfile 2>/dev/null; cargo audit); then
status=1
fi
echo "::endgroup::"
done < <(find . -name 'Cargo.toml' -not -path '*/target/*' -exec dirname {} \; | sort -u)
exit $status

audit-pip:
name: pip-audit
needs: detect
if: needs.detect.outputs.pip == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.x"

- name: Install pip-audit
run: pip install pip-audit==2.9.0

- name: Audit Python dependencies
run: |
status=0
# Audit each Python project found in the repo
while IFS= read -r dir; do
echo "::group::pip-audit $dir"
if [ -f "$dir/pyproject.toml" ]; then
if ! pip-audit "$dir"; then
status=1
fi
elif [ -f "$dir/requirements.txt" ]; then
if ! pip-audit -r "$dir/requirements.txt"; then
status=1
fi
fi
echo "::endgroup::"
done < <(
{
find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \;
find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \;
} | sort -u
)
exit $status
27 changes: 26 additions & 1 deletion src/cli/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ use super::common;
#[derive(Args, Debug, Clone, Default)]
pub struct SyncArgs {
/// Continue a paused restack rebase sequence
#[arg(short = 'c', long = "continue")]
#[arg(short = 'c', long = "continue", conflicts_with = "abort_operation")]
pub continue_operation: bool,

/// Abort a paused restack rebase sequence
#[arg(long = "abort", conflicts_with = "continue_operation")]
pub abort_operation: bool,
}

pub fn execute(args: SyncArgs) -> io::Result<CommandOutcome> {
Expand Down Expand Up @@ -261,6 +265,13 @@ pub fn execute(args: SyncArgs) -> io::Result<CommandOutcome> {
}
}

if args.abort_operation && outcome.status.success() {
println!("Aborted paused restack operation.");
return Ok(CommandOutcome {
status: outcome.status,
});
}

if !outcome.status.success() {
if outcome.paused {
common::print_restack_pause_guidance(outcome.failure_output.as_deref());
Expand Down Expand Up @@ -434,6 +445,7 @@ impl From<SyncArgs> for SyncOptions {
fn from(args: SyncArgs) -> Self {
Self {
continue_operation: args.continue_operation,
abort_operation: args.abort_operation,
}
}
}
Expand Down Expand Up @@ -550,8 +562,21 @@ mod tests {
fn converts_cli_args_into_core_sync_options() {
let options = SyncOptions::from(SyncArgs {
continue_operation: true,
abort_operation: false,
});

assert!(options.continue_operation);
assert!(!options.abort_operation);
}

#[test]
fn converts_abort_cli_args_into_core_sync_options() {
let options = SyncOptions::from(SyncArgs {
continue_operation: false,
abort_operation: true,
});

assert!(!options.continue_operation);
assert!(options.abort_operation);
}
}
8 changes: 8 additions & 0 deletions src/core/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ pub fn continue_rebase() -> io::Result<GitCommandOutput> {
output_to_git_command_output(output)
}

pub fn abort_rebase() -> io::Result<GitCommandOutput> {
let output = Command::new("git")
.args(["rebase", "--abort"])
.output()?;

output_to_git_command_output(output)
}

pub fn init_repository() -> io::Result<ExitStatus> {
Command::new("git").args(["init", "--quiet"]).status()
}
Expand Down
Loading