Compare commits

..
11 Commits
Author SHA1 Message Date
Kevin Veen-BirkenbachandClaude Opus 4.7 70b06d2b3a chore(config): refresh default repository list
CI / security-codeql (push) Has been cancelled
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-env-virtual (push) Has been cancelled
CI / test-env-nix (push) Has been cancelled
CI / test-e2e (push) Has been cancelled
CI / test-virgin-user (push) Has been cancelled
CI / test-virgin-root (push) Has been cancelled
CI / lint-shell (push) Has been cancelled
CI / lint-python (push) Has been cancelled
CI / lint-docker (push) Has been cancelled
Drops the `analysis-ready-code` entry and renames the `infinito-nexus`
default to `infinito-nexus/core`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:19:03 +02:00
Kevin Veen-BirkenbachandClaude Opus 4.7 00c668b595 chore(claude): expand permissions and require sandbox
- Adds `Bash(*)` to the allow list so routine shell commands run without
  prompting.
- Sets `sandbox.failIfUnavailable=true` so Claude Code aborts rather
  than silently running unsandboxed when the sandbox cannot initialize.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:18:54 +02:00
Kevin Veen-BirkenbachandClaude Opus 4.7 12a38b7e6a fix(nix): clear stale wheels before pypaBuildPhase
`dist/` carried in via the source tree can contain a stale wheel from a
previous build (e.g. kpmx-1.12.1 alongside the freshly built 1.13.3).
Both wheels declare a `bin/pkgmgr` entry, so `pypaInstallPhase` hits
FileExistsError on the second install. Wipe `dist/` in `preBuild` so
only the fresh wheel is installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:18:43 +02:00
Kevin Veen-BirkenbachandClaude Opus 4.7 37fd2192a5 feat(pull,push): parallel execution via --jobs flag
Adds `pkgmgr pull -j N` and `pkgmgr push -j N` for concurrent operation
across repositories (default: min(cpu_count, 8), use 1 for sequential).
Verification in pull also parallelizes; interactive prompts and the
actual git command still run on the main thread. Shared parallel-runner
and repo-resolution helpers live in a new `_parallel.py` module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:18:31 +02:00
Kevin Veen-BirkenbachandClaude Opus 4.7 607102e7f8 chore(claude): add project settings with sandbox and ask rules
CI / security-codeql (push) Has been cancelled
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-env-virtual (push) Has been cancelled
CI / test-env-nix (push) Has been cancelled
CI / test-e2e (push) Has been cancelled
CI / test-virgin-user (push) Has been cancelled
CI / test-virgin-root (push) Has been cancelled
CI / lint-shell (push) Has been cancelled
CI / lint-python (push) Has been cancelled
CI / lint-docker (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:54:34 +02:00
Kevin Veen-Birkenbach 133cf63b9f Release version 1.13.3
Mark stable commit / mark-stable (push) Canceled after 0s
2026-03-26 17:10:21 +01:00
Kevin Veen-Birkenbach 6334936e8a fix(ci): resolve workflow and docker scan findings 2026-03-26 16:44:02 +01:00
Kevin Veen-Birkenbach 946965f016 fix(ci): grant reusable workflows security permissions 2026-03-26 16:33:40 +01:00
Kevin Veen-Birkenbach 541a7f679f feat(ci): add docker lint and codeql workflows 2026-03-26 16:30:36 +01:00
Kevin Veen-Birkenbach 128f71745a refactor(ci): organize workflow scripts and gate publish on main 2026-03-26 15:58:18 +01:00
Kevin Veen-Birkenbach df2ce636c8 fix(ci): make mark-stable main-only and cancel stale runs 2026-03-26 14:57:04 +01:00
37 changed files with 678 additions and 176 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"Bash(*)"
],
"ask": [
"Skill(update-config)",
"Skill(update-config:*)"
]
},
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"autoAllowBashIfSandboxed": true
}
}
+34
View File
@@ -6,34 +6,68 @@ on:
- '**' - '**'
pull_request: pull_request:
permissions:
contents: read
concurrency: concurrency:
group: global-ci-${{ github.repository }}-${{ github.ref_name }} group: global-ci-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
security-codeql:
permissions:
contents: read
packages: read
security-events: write
uses: ./.github/workflows/security-codeql.yml
test-unit: test-unit:
permissions:
contents: read
uses: ./.github/workflows/test-unit.yml uses: ./.github/workflows/test-unit.yml
test-integration: test-integration:
permissions:
contents: read
uses: ./.github/workflows/test-integration.yml uses: ./.github/workflows/test-integration.yml
test-env-virtual: test-env-virtual:
permissions:
contents: read
uses: ./.github/workflows/test-env-virtual.yml uses: ./.github/workflows/test-env-virtual.yml
test-env-nix: test-env-nix:
permissions:
contents: read
uses: ./.github/workflows/test-env-nix.yml uses: ./.github/workflows/test-env-nix.yml
test-e2e: test-e2e:
permissions:
contents: read
uses: ./.github/workflows/test-e2e.yml uses: ./.github/workflows/test-e2e.yml
test-virgin-user: test-virgin-user:
permissions:
contents: read
uses: ./.github/workflows/test-virgin-user.yml uses: ./.github/workflows/test-virgin-user.yml
test-virgin-root: test-virgin-root:
permissions:
contents: read
uses: ./.github/workflows/test-virgin-root.yml uses: ./.github/workflows/test-virgin-root.yml
lint-shell: lint-shell:
permissions:
contents: read
uses: ./.github/workflows/lint-shell.yml uses: ./.github/workflows/lint-shell.yml
lint-python: lint-python:
permissions:
contents: read
uses: ./.github/workflows/lint-python.yml uses: ./.github/workflows/lint-python.yml
lint-docker:
permissions:
contents: read
security-events: write
uses: ./.github/workflows/lint-docker.yml
+40
View File
@@ -0,0 +1,40 @@
name: Docker Linter
on:
workflow_call:
permissions:
contents: read
jobs:
lint-docker:
name: Lint Dockerfile
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run hadolint (produce SARIF)
id: hadolint
continue-on-error: true
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5
with:
dockerfile: ./Dockerfile
format: sarif
output-file: hadolint-results.sarif
failure-threshold: warning
- name: Upload analysis results to GitHub
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: hadolint-results.sarif
wait-for-processing: true
category: hadolint
- name: Fail if SARIF contains warnings or errors
if: always()
run: python3 src/pkgmgr/github/check_hadolint_sarif.py hadolint-results.sarif
+3
View File
@@ -3,6 +3,9 @@ name: Ruff (Python code sniffer)
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
lint-python: lint-python:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -3,6 +3,9 @@ name: ShellCheck
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
lint-shell: lint-shell:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+16 -96
View File
@@ -1,8 +1,8 @@
name: Mark stable commit name: Mark stable commit
concurrency: concurrency:
group: mark-${{ github.repository }}-${{ github.ref_name }} group: mark-stable-${{ github.repository }}-main
cancel-in-progress: false cancel-in-progress: true
on: on:
push: push:
@@ -11,109 +11,29 @@ on:
jobs: jobs:
mark-stable: mark-stable:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 330
permissions: permissions:
actions: read actions: read
contents: write contents: write
steps: steps:
- name: Wait for CI success on main for this commit
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SHA="${GITHUB_SHA}"
API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${SHA}&event=push&per_page=20"
WAIT_INTERVAL_SECONDS=20
MAX_ATTEMPTS=360 # 2 hours max wait
STATUS=""
CONCLUSION=""
echo "Waiting for CI on main for ${SHA} (up to 2 hours)..."
for attempt in $(seq 1 "${MAX_ATTEMPTS}"); do
RESPONSE="$(curl -fsSL \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"${API_URL}")"
STATUS="$(printf '%s' "${RESPONSE}" | jq -r '.workflow_runs[] | select(.head_branch=="main") | .status' | head -n1)"
CONCLUSION="$(printf '%s' "${RESPONSE}" | jq -r '.workflow_runs[] | select(.head_branch=="main") | .conclusion' | head -n1)"
if [[ -n "${STATUS}" ]]; then
echo "CI status=${STATUS} conclusion=${CONCLUSION:-none} (attempt ${attempt}/${MAX_ATTEMPTS})"
else
echo "No CI run for main found yet (attempt ${attempt}/${MAX_ATTEMPTS})"
fi
if [[ "${STATUS}" == "completed" ]]; then
if [[ "${CONCLUSION}" == "success" ]]; then
echo "CI succeeded for ${SHA}."
break
fi
echo "CI failed for ${SHA} (conclusion=${CONCLUSION})."
exit 1
fi
sleep "${WAIT_INTERVAL_SECONDS}"
done
if [[ "${STATUS}" != "completed" || "${CONCLUSION}" != "success" ]]; then
echo "Timed out waiting for successful CI on main for ${SHA}."
exit 1
fi
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
fetch-tags: true # We need all tags for version comparison fetch-tags: true # We need tags and main history for version comparison
- name: Check whether tagged commit is on main
id: branch-check
run: bash scripts/github/common/check-tagged-commit-on-main.sh
- name: Wait for CI success on main for this commit
if: steps.branch-check.outputs.is_on_main == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: bash scripts/github/mark-stable/wait-for-main-ci-success.sh
- name: Move 'stable' tag only if this version is the highest - name: Move 'stable' tag only if this version is the highest
run: | if: steps.branch-check.outputs.is_on_main == 'true'
set -euo pipefail run: bash scripts/github/mark-stable/mark-stable-if-highest-version.sh
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
echo "Ref: $GITHUB_REF"
echo "SHA: $GITHUB_SHA"
VERSION="${GITHUB_REF#refs/tags/}"
echo "Current version tag: ${VERSION}"
echo "Collecting all version tags..."
ALL_V_TAGS="$(git tag --list 'v*' || true)"
if [[ -z "${ALL_V_TAGS}" ]]; then
echo "No version tags found. Skipping stable update."
exit 0
fi
echo "All version tags:"
echo "${ALL_V_TAGS}"
# Determine highest version using natural version sorting
LATEST_TAG="$(printf '%s\n' ${ALL_V_TAGS} | sort -V | tail -n1)"
echo "Highest version tag: ${LATEST_TAG}"
if [[ "${VERSION}" != "${LATEST_TAG}" ]]; then
echo "Current version ${VERSION} is NOT the highest version."
echo "Stable tag will NOT be updated."
exit 0
fi
echo "Current version ${VERSION} IS the highest version."
echo "Updating 'stable' tag..."
# Delete existing stable tag (local + remote)
git tag -d stable 2>/dev/null || true
git push origin :refs/tags/stable || true
# Create new stable tag
git tag stable "$GITHUB_SHA"
git push origin stable
echo "✅ Stable tag updated to ${VERSION}."
+18 -33
View File
@@ -21,44 +21,30 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Checkout workflow_run commit and refresh tags - name: Checkout workflow_run commit and refresh tags
run: | env:
set -euo pipefail WORKFLOW_RUN_SHA: ${{ github.event.workflow_run.head_sha }}
git checkout -f "${{ github.event.workflow_run.head_sha }}" run: bash scripts/github/publish-containers/checkout-workflow-run-commit.sh
git fetch --tags --force
git tag --list 'stable' 'v*' --sort=version:refname | tail -n 20 - name: Check whether tagged commit is on main
id: branch-check
env:
TARGET_SHA: ${{ github.event.workflow_run.head_sha }}
run: bash scripts/github/common/check-tagged-commit-on-main.sh
- name: Compute version and stable flag - name: Compute version and stable flag
id: info id: info
run: | if: steps.branch-check.outputs.is_on_main == 'true'
set -euo pipefail run: bash scripts/github/publish-containers/compute-publish-container-info.sh
SHA="$(git rev-parse HEAD)"
V_TAG="$(git tag --points-at "${SHA}" --list 'v*' | sort -V | tail -n1)"
if [[ -z "${V_TAG}" ]]; then
echo "No version tag found for ${SHA}. Skipping publish."
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
VERSION="${V_TAG#v}"
STABLE_SHA="$(git rev-parse -q --verify refs/tags/stable^{commit} 2>/dev/null || true)"
IS_STABLE=false
[[ -n "${STABLE_SHA}" && "${STABLE_SHA}" == "${SHA}" ]] && IS_STABLE=true
echo "should_publish=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "is_stable=${IS_STABLE}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx - name: Set up Docker Buildx
if: ${{ steps.info.outputs.should_publish == 'true' }} if: ${{ steps.info.outputs.should_publish == 'true' }}
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
with: with:
use: true use: true
- name: Login to GHCR - name: Login to GHCR
if: ${{ steps.info.outputs.should_publish == 'true' }} if: ${{ steps.info.outputs.should_publish == 'true' }}
uses: docker/login-action@v3 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -66,9 +52,8 @@ jobs:
- name: Publish all images - name: Publish all images
if: ${{ steps.info.outputs.should_publish == 'true' }} if: ${{ steps.info.outputs.should_publish == 'true' }}
run: | env:
set -euo pipefail OWNER: ${{ github.repository_owner }}
OWNER="${{ github.repository_owner }}" \ VERSION: ${{ steps.info.outputs.version }}
VERSION="${{ steps.info.outputs.version }}" \ IS_STABLE: ${{ steps.info.outputs.is_stable }}
IS_STABLE="${{ steps.info.outputs.is_stable }}" \ run: bash scripts/github/publish-containers/publish-container-images.sh
bash scripts/build/publish.sh
+47
View File
@@ -0,0 +1,47 @@
name: CodeQL Advanced
on:
workflow_call:
jobs:
analyze:
name: Check security
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: python
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
queries: security-extended,security-and-quality
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code.'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
+3
View File
@@ -3,6 +3,9 @@ name: Test End-To-End
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
test-e2e: test-e2e:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -3,6 +3,9 @@ name: Test Virgin Nix (flake only)
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
test-env-nix: test-env-nix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -3,6 +3,9 @@ name: Test OS Containers
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
test-env-virtual: test-env-virtual:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -3,6 +3,9 @@ name: Test Code Integration
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
test-integration: test-integration:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -3,6 +3,9 @@ name: Test Units
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
test-unit: test-unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -3,6 +3,9 @@ name: Test Virgin Root
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
test-virgin-root: test-virgin-root:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -3,6 +3,9 @@ name: Test Virgin User
on: on:
workflow_call: workflow_call:
permissions:
contents: read
jobs: jobs:
test-virgin-user: test-virgin-user:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+9
View File
@@ -1,3 +1,12 @@
## [1.13.3] - 2026-03-26
* CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations
* Workflow permissions were tightened and fixed, ensuring secure and reliable execution of reusable workflows
* Publishing and “stable” tagging are now restricted to the `main` branch, preventing accidental releases from other branches
* Stale CI runs are automatically cancelled, reducing wasted resources and speeding up feedback cycles
* Overall CI reliability and security posture improved, with fewer false positives and more consistent pipeline results
## [1.13.2] - 2026-03-26 ## [1.13.2] - 2026-03-26
* Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path. * Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path.
+3 -4
View File
@@ -43,10 +43,10 @@ WORKDIR /build
COPY . . COPY . .
# Build and install distro-native package-manager package # Build and install distro-native package-manager package
RUN set -euo pipefail; \ RUN set -eu; \
echo "Building and installing package-manager via make install..."; \ echo "Building and installing package-manager via make install..."; \
make install; \ make install; \
cd /; rm -rf /build rm -rf /build
# Entry point # Entry point
COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh
@@ -64,5 +64,4 @@ CMD ["pkgmgr", "--help"]
FROM full AS slim FROM full AS slim
COPY scripts/docker/slim.sh /usr/local/bin/slim.sh COPY scripts/docker/slim.sh /usr/local/bin/slim.sh
RUN chmod +x /usr/local/bin/slim.sh RUN chmod +x /usr/local/bin/slim.sh && /usr/local/bin/slim.sh
RUN /usr/local/bin/slim.sh
+5 -1
View File
@@ -32,7 +32,7 @@
rec { rec {
pkgmgr = pyPkgs.buildPythonApplication { pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager"; pname = "package-manager";
version = "1.13.2"; version = "1.13.3";
# Use the git repo as source # Use the git repo as source
src = ./.; src = ./.;
@@ -40,6 +40,10 @@
# Build using pyproject.toml # Build using pyproject.toml
format = "pyproject"; format = "pyproject";
# Clear any stale wheels carried in from the source tree so
# pypaInstallPhase doesn't collide on bin/pkgmgr.
preBuild = "rm -rf dist";
# Build backend requirements from [build-system] # Build backend requirements from [build-system]
nativeBuildInputs = [ nativeBuildInputs = [
pyPkgs.setuptools pyPkgs.setuptools
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world> # Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager pkgname=package-manager
pkgver=1.13.2 pkgver=1.13.3
pkgrel=1 pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)." pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any') arch=('any')
+10
View File
@@ -1,3 +1,13 @@
package-manager (1.13.3-1) unstable; urgency=medium
* CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations
* Workflow permissions were tightened and fixed, ensuring secure and reliable execution of reusable workflows
* Publishing and “stable” tagging are now restricted to the `main` branch, preventing accidental releases from other branches
* Stale CI runs are automatically cancelled, reducing wasted resources and speeding up feedback cycles
* Overall CI reliability and security posture improved, with fewer false positives and more consistent pipeline results
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 26 Mar 2026 17:10:21 +0100
package-manager (1.13.2-1) unstable; urgency=medium package-manager (1.13.2-1) unstable; urgency=medium
* Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path. * Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path.
+8 -1
View File
@@ -1,5 +1,5 @@
Name: package-manager Name: package-manager
Version: 1.13.2 Version: 1.13.3
Release: 1%{?dist} Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -74,6 +74,13 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/ /usr/lib/package-manager/
%changelog %changelog
* Thu Mar 26 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.3-1
- CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations
* Workflow permissions were tightened and fixed, ensuring secure and reliable execution of reusable workflows
* Publishing and “stable” tagging are now restricted to the `main` branch, preventing accidental releases from other branches
* Stale CI runs are automatically cancelled, reducing wasted resources and speeding up feedback cycles
* Overall CI reliability and security posture improved, with fewer false positives and more consistent pipeline results
* Thu Mar 26 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.2-1 * Thu Mar 26 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.2-1
- Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path. - Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path.
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kpmx" name = "kpmx"
version = "1.13.2" version = "1.13.3"
description = "Kevin's package-manager tool (pkgmgr)" description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET_SHA="${TARGET_SHA:-${GITHUB_SHA:?GITHUB_SHA must be set}}"
git fetch --no-tags origin main
if git merge-base --is-ancestor "${TARGET_SHA}" "origin/main"; then
echo "is_on_main=true" >> "$GITHUB_OUTPUT"
echo "Target commit ${TARGET_SHA} is contained in origin/main."
else
echo "is_on_main=false" >> "$GITHUB_OUTPUT"
echo "Target commit ${TARGET_SHA} is not contained in origin/main. Skipping main-only action."
fi
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
echo "Ref: $GITHUB_REF"
echo "SHA: $GITHUB_SHA"
VERSION="${GITHUB_REF#refs/tags/}"
echo "Current version tag: ${VERSION}"
echo "Collecting all version tags..."
ALL_V_TAGS="$(git tag --list 'v*' || true)"
if [[ -z "${ALL_V_TAGS}" ]]; then
echo "No version tags found. Skipping stable update."
exit 0
fi
echo "All version tags:"
echo "${ALL_V_TAGS}"
LATEST_TAG="$(printf '%s\n' "${ALL_V_TAGS}" | sort -V | tail -n1)"
echo "Highest version tag: ${LATEST_TAG}"
if [[ "${VERSION}" != "${LATEST_TAG}" ]]; then
echo "Current version ${VERSION} is NOT the highest version."
echo "Stable tag will NOT be updated."
exit 0
fi
echo "Current version ${VERSION} IS the highest version."
echo "Updating 'stable' tag..."
git tag -d stable 2>/dev/null || true
git push origin :refs/tags/stable || true
git tag stable "$GITHUB_SHA"
git push origin stable
echo "Stable tag updated to ${VERSION}."
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
SHA="${GITHUB_SHA}"
API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${SHA}&event=push&per_page=20"
WAIT_INTERVAL_SECONDS=20
MAX_ATTEMPTS=990 # 5 hours 30 minutes max wait
STATUS=""
CONCLUSION=""
echo "Waiting for CI on main for ${SHA} (up to 5 hours 30 minutes)..."
for attempt in $(seq 1 "${MAX_ATTEMPTS}"); do
RESPONSE="$(curl -fsSL \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"${API_URL}")"
STATUS="$(printf '%s' "${RESPONSE}" | jq -r '.workflow_runs[] | select(.head_branch=="main") | .status' | head -n1)"
CONCLUSION="$(printf '%s' "${RESPONSE}" | jq -r '.workflow_runs[] | select(.head_branch=="main") | .conclusion' | head -n1)"
if [[ -n "${STATUS}" ]]; then
echo "CI status=${STATUS} conclusion=${CONCLUSION:-none} (attempt ${attempt}/${MAX_ATTEMPTS})"
else
echo "No CI run for main found yet (attempt ${attempt}/${MAX_ATTEMPTS})"
fi
if [[ "${STATUS}" == "completed" ]]; then
if [[ "${CONCLUSION}" == "success" ]]; then
echo "CI succeeded for ${SHA}."
break
fi
echo "CI failed for ${SHA} (conclusion=${CONCLUSION})."
exit 1
fi
sleep "${WAIT_INTERVAL_SECONDS}"
done
if [[ "${STATUS}" != "completed" || "${CONCLUSION}" != "success" ]]; then
echo "Timed out waiting for successful CI on main for ${SHA}."
exit 1
fi
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
WORKFLOW_RUN_SHA="${WORKFLOW_RUN_SHA:?WORKFLOW_RUN_SHA must be set}"
git checkout -f "${WORKFLOW_RUN_SHA}"
git fetch --tags --force
git tag --list 'stable' 'v*' --sort=version:refname | tail -n 20
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
SHA="$(git rev-parse HEAD)"
V_TAG="$(git tag --points-at "${SHA}" --list 'v*' | sort -V | tail -n1)"
if [[ -z "${V_TAG}" ]]; then
echo "No version tag found for ${SHA}. Skipping publish."
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
VERSION="${V_TAG#v}"
STABLE_SHA="$(git rev-parse -q --verify 'refs/tags/stable^{commit}' 2>/dev/null || true)"
IS_STABLE=false
[[ -n "${STABLE_SHA}" && "${STABLE_SHA}" == "${SHA}" ]] && IS_STABLE=true
{
echo "should_publish=true"
echo "version=${VERSION}"
echo "is_stable=${IS_STABLE}"
} >> "$GITHUB_OUTPUT"
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
: "${OWNER:?OWNER must be set}"
: "${VERSION:?VERSION must be set}"
: "${IS_STABLE:?IS_STABLE must be set}"
bash scripts/build/publish.sh
@@ -0,0 +1,91 @@
from __future__ import annotations
import os
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Callable, Dict, List, Tuple
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.identifier import get_repo_identifier
Repository = Dict[str, Any]
RepoRef = Tuple[str, str]
OpResult = Tuple[bool, str]
RepoOp = Callable[[str], OpResult]
def resolve_repos(
selected_repos: List[Repository],
repositories_base_dir: str,
all_repos: List[Repository],
) -> List[RepoRef]:
"""
Resolve ``(identifier, repo_dir)`` pairs for ``selected_repos``.
Repositories whose directory does not exist on disk are reported and
skipped, matching the prior behavior of pull/push handlers.
"""
resolved: List[RepoRef] = []
for repo in selected_repos:
ident = get_repo_identifier(repo, all_repos)
rd = get_repo_dir(repositories_base_dir, repo)
if not os.path.exists(rd):
print(f"Repository directory '{rd}' not found for {ident}.")
continue
resolved.append((ident, rd))
return resolved
def run_on_repos(
repos: List[RepoRef],
op: RepoOp,
*,
jobs: int,
op_name: str,
) -> None:
"""
Run ``op(repo_dir) -> (ok, msg)`` for each repo, optionally in parallel.
- ``jobs == 1``: serial, quiet on success, prints ``msg`` on failure.
- ``jobs > 1``: parallel via ThreadPoolExecutor, prints a banner plus
``[OK]``/``[FAIL]`` per repo and a final summary.
- Exits with status 1 if any operation failed.
"""
if not repos:
return
effective_jobs = max(1, min(jobs, len(repos)))
failed: List[Tuple[str, str]] = []
if effective_jobs == 1:
for ident, rd in repos:
ok, msg = op(rd)
if not ok:
print(msg)
failed.append((ident, msg))
else:
print(
f"[{op_name.upper()}] Running {len(repos)} {op_name}(s) with up to "
f"{effective_jobs} parallel jobs..."
)
with ThreadPoolExecutor(max_workers=effective_jobs) as executor:
futures = {executor.submit(op, rd): ident for ident, rd in repos}
for future in as_completed(futures):
ident = futures[future]
ok, msg = future.result()
if ok:
print(f"[OK] {ident}")
else:
print(f"[FAIL] {ident}")
for line in msg.splitlines():
print(f" {line}")
failed.append((ident, msg))
if failed:
if effective_jobs > 1:
print(
f"\n[SUMMARY] {len(failed)} of {len(repos)} {op_name}(s) failed:"
)
for ident, _msg in failed:
print(f" - {ident}")
sys.exit(1)
+79 -26
View File
@@ -1,17 +1,66 @@
from __future__ import annotations from __future__ import annotations
import os import os
import sys from concurrent.futures import ThreadPoolExecutor
from typing import List, Dict, Any from typing import Any, Dict, List, Tuple
from pkgmgr.actions.repository._parallel import RepoRef, run_on_repos
from pkgmgr.core.git.commands import pull_args, GitPullArgsError from pkgmgr.core.git.commands import pull_args, GitPullArgsError
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.identifier import get_repo_identifier from pkgmgr.core.repository.identifier import get_repo_identifier
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.verify import verify_repository from pkgmgr.core.repository.verify import verify_repository
Repository = Dict[str, Any] Repository = Dict[str, Any]
def _pull_one(repo_dir: str, extra_args: List[str], preview: bool) -> Tuple[bool, str]:
try:
pull_args(extra_args, cwd=repo_dir, preview=preview)
return (True, "")
except GitPullArgsError as exc:
return (False, str(exc))
def _verify_one(
repo: Repository,
repo_dir: str,
no_verification: bool,
) -> Tuple[bool, bool, List[str]]:
"""Returns (has_verified_info, verified_ok, errors)."""
verified_ok, errors, _commit, _key = verify_repository(
repo, repo_dir, mode="pull", no_verification=no_verification,
)
return (bool(repo.get("verified")), verified_ok, errors)
def _verify_all(
candidates: List[Tuple[Repository, str, str]],
no_verification: bool,
jobs: int,
) -> List[Tuple[str, str, bool, bool, List[str]]]:
"""
Verify all candidates (parallel if ``jobs > 1``), preserving input order.
Returns one tuple per candidate: ``(ident, repo_dir, has_verified_info,
verified_ok, errors)``.
"""
verify_jobs = max(1, min(jobs, len(candidates)))
if verify_jobs == 1:
return [
(ident, rd, *_verify_one(repo, rd, no_verification))
for repo, ident, rd in candidates
]
with ThreadPoolExecutor(max_workers=verify_jobs) as executor:
futures = [
executor.submit(_verify_one, repo, rd, no_verification)
for repo, _ident, rd in candidates
]
results = [f.result() for f in futures]
return [
(ident, rd, *res) for (_repo, ident, rd), res in zip(candidates, results)
]
def pull_with_verification( def pull_with_verification(
selected_repos: List[Repository], selected_repos: List[Repository],
repositories_base_dir: str, repositories_base_dir: str,
@@ -19,41 +68,45 @@ def pull_with_verification(
extra_args: List[str], extra_args: List[str],
no_verification: bool, no_verification: bool,
preview: bool, preview: bool,
jobs: int = 1,
) -> None: ) -> None:
""" """
Execute `git pull` for each repository with verification. Execute `git pull` for each repository with verification.
- If verification fails and verification is enabled, prompt user to continue. - Verification (I/O-bound) runs in parallel when ``jobs > 1``.
- Uses core.git.commands.pull_args() (no raw subprocess usage). - Interactive prompts for failed verifications are handled serially on the
main thread after parallel verification completes.
- Approved repos are then pulled in parallel when ``jobs > 1``.
- On any pull failure, prints a summary and exits with status 1.
""" """
candidates: List[Tuple[Repository, str, str]] = []
for repo in selected_repos: for repo in selected_repos:
repo_identifier = get_repo_identifier(repo, all_repos) ident = get_repo_identifier(repo, all_repos)
repo_dir = get_repo_dir(repositories_base_dir, repo) rd = get_repo_dir(repositories_base_dir, repo)
if not os.path.exists(rd):
if not os.path.exists(repo_dir): print(f"Repository directory '{rd}' not found for {ident}.")
print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.")
continue continue
candidates.append((repo, ident, rd))
verified_info = repo.get("verified") if not candidates:
verified_ok, errors, _commit_hash, _signing_key = verify_repository( return
repo,
repo_dir,
mode="pull",
no_verification=no_verification,
)
if not preview and not no_verification and verified_info and not verified_ok: verify_results = _verify_all(candidates, no_verification, jobs)
print(f"Warning: Verification failed for {repo_identifier}:")
approved: List[RepoRef] = []
for ident, rd, has_verified_info, verified_ok, errors in verify_results:
if not preview and not no_verification and has_verified_info and not verified_ok:
print(f"Warning: Verification failed for {ident}:")
for err in errors: for err in errors:
print(f" - {err}") print(f" - {err}")
choice = input("Proceed with 'git pull'? (y/N): ").strip().lower() choice = input("Proceed with 'git pull'? (y/N): ").strip().lower()
if choice != "y": if choice != "y":
continue continue
approved.append((ident, rd))
try: run_on_repos(
pull_args(extra_args, cwd=repo_dir, preview=preview) approved,
except GitPullArgsError as exc: lambda rd: _pull_one(rd, extra_args, preview),
# Keep behavior consistent with previous implementation: jobs=jobs,
# stop on first failure and propagate return code as generic failure. op_name="pull",
print(str(exc)) )
sys.exit(1)
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from typing import Any, Dict, List, Tuple
from pkgmgr.actions.repository._parallel import (
resolve_repos,
run_on_repos,
)
from pkgmgr.core.git.commands import push_args, GitPushArgsError
Repository = Dict[str, Any]
def _push_one(repo_dir: str, extra_args: List[str], preview: bool) -> Tuple[bool, str]:
try:
push_args(extra_args, cwd=repo_dir, preview=preview)
return (True, "")
except GitPushArgsError as exc:
return (False, str(exc))
def push_in_parallel(
selected_repos: List[Repository],
repositories_base_dir: str,
all_repos: List[Repository],
extra_args: List[str],
preview: bool,
jobs: int = 1,
) -> None:
"""
Execute `git push` for each repository, optionally in parallel.
"""
repos = resolve_repos(selected_repos, repositories_base_dir, all_repos)
run_on_repos(
repos,
lambda rd: _push_one(rd, extra_args, preview),
jobs=jobs,
op_name="push",
)
+22
View File
@@ -12,6 +12,7 @@ from pkgmgr.cli.context import CLIContext
from pkgmgr.actions.repository.clone import clone_repos from pkgmgr.actions.repository.clone import clone_repos
from pkgmgr.actions.proxy import exec_proxy_command from pkgmgr.actions.proxy import exec_proxy_command
from pkgmgr.actions.repository.pull import pull_with_verification from pkgmgr.actions.repository.pull import pull_with_verification
from pkgmgr.actions.repository.push import push_in_parallel
from pkgmgr.core.repository.selected import get_selected_repos from pkgmgr.core.repository.selected import get_selected_repos
from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.dir import get_repo_dir
@@ -177,6 +178,17 @@ def register_proxy_commands(
default=False, default=False,
help="Disable verification via commit/gpg", help="Disable verification via commit/gpg",
) )
if subcommand in ("pull", "push"):
parser.add_argument(
"-j",
"--jobs",
type=int,
default=min(os.cpu_count() or 4, 8),
help=(
f"Number of parallel {subcommand}s "
"(default: min(cpu_count, 8)). Use 1 for sequential."
),
)
if subcommand == "clone": if subcommand == "clone":
parser.add_argument( parser.add_argument(
"--clone-mode", "--clone-mode",
@@ -234,6 +246,16 @@ def maybe_handle_proxy(args: argparse.Namespace, ctx: CLIContext) -> bool:
args.extra_args, args.extra_args,
args.no_verification, args.no_verification,
args.preview, args.preview,
jobs=args.jobs,
)
elif args.command == "push":
push_in_parallel(
selected,
ctx.repositories_base_dir,
ctx.all_repositories,
args.extra_args,
args.preview,
jobs=args.jobs,
) )
else: else:
exec_proxy_command( exec_proxy_command(
+2 -13
View File
@@ -5,16 +5,6 @@ directories:
workspaces: ~/Workspaces/ workspaces: ~/Workspaces/
binaries: ~/.local/bin/ binaries: ~/.local/bin/
repositories: repositories:
- account: kevinveenbirkenbach
alias: arc
provider: github.com
repository: analysis-ready-code
description: Analysis-Ready Code (ARC) is a Python utility that recursively scans directories and transforms source code into a streamlined, analysis-ready format by removing comments, filtering files, and compressing content—perfect for AI and automated code analysis.
homepage: https://github.com/kevinveenbirkenbach/analysis-ready-code
verified:
gpg_keys:
- 44D8F11FD62F878E
- B5690EEEBB952194
- account: kevinveenbirkenbach - account: kevinveenbirkenbach
description: A configurable Python package manager that automates repository tasks—including cloning, installation, updates, and status reporting—based on a YAML configuration file for streamlined software management which gives you access to the Kevin Veen-Birkenbach Code Universe. description: A configurable Python package manager that automates repository tasks—including cloning, installation, updates, and status reporting—based on a YAML configuration file for streamlined software management which gives you access to the Kevin Veen-Birkenbach Code Universe.
homepage: https://github.com/kevinveenbirkenbach/package-manager homepage: https://github.com/kevinveenbirkenbach/package-manager
@@ -274,12 +264,11 @@ repositories:
gpg_keys: gpg_keys:
- 44D8F11FD62F878E - 44D8F11FD62F878E
- B5690EEEBB952194 - B5690EEEBB952194
- account: kevinveenbirkenbach - account: infinito-nexus
alias: infinito
provider: github.com provider: github.com
description: Infinito.nexus streamlines Linux-based system setups and Docker image administration, perfect for servers and PCs. It offers extensive solutions for system initialization, admin tools, backups, monitoring, updates, driver management, security, and VPNs. description: Infinito.nexus streamlines Linux-based system setups and Docker image administration, perfect for servers and PCs. It offers extensive solutions for system initialization, admin tools, backups, monitoring, updates, driver management, security, and VPNs.
homepage: https://infinito.nexus homepage: https://infinito.nexus
repository: infinito-nexus repository: core
verified: verified:
gpg_keys: gpg_keys:
- 44D8F11FD62F878E - 44D8F11FD62F878E
+3
View File
@@ -19,6 +19,7 @@ from .pull import GitPullError, pull
from .pull_args import GitPullArgsError, pull_args from .pull_args import GitPullArgsError, pull_args
from .pull_ff_only import GitPullFfOnlyError, pull_ff_only from .pull_ff_only import GitPullFfOnlyError, pull_ff_only
from .push import GitPushError, push from .push import GitPushError, push
from .push_args import GitPushArgsError, push_args
from .push_upstream import GitPushUpstreamError, push_upstream from .push_upstream import GitPushUpstreamError, push_upstream
from .set_remote_url import GitSetRemoteUrlError, set_remote_url from .set_remote_url import GitSetRemoteUrlError, set_remote_url
from .tag_annotated import GitTagAnnotatedError, tag_annotated from .tag_annotated import GitTagAnnotatedError, tag_annotated
@@ -34,6 +35,7 @@ __all__ = [
"pull_ff_only", "pull_ff_only",
"merge_no_ff", "merge_no_ff",
"push", "push",
"push_args",
"commit", "commit",
"delete_local_branch", "delete_local_branch",
"delete_remote_branch", "delete_remote_branch",
@@ -56,6 +58,7 @@ __all__ = [
"GitPullFfOnlyError", "GitPullFfOnlyError",
"GitMergeError", "GitMergeError",
"GitPushError", "GitPushError",
"GitPushArgsError",
"GitCommitError", "GitCommitError",
"GitDeleteLocalBranchError", "GitDeleteLocalBranchError",
"GitDeleteRemoteBranchError", "GitDeleteRemoteBranchError",
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from typing import List
from ..errors import GitRunError, GitCommandError
from ..run import run
class GitPushArgsError(GitCommandError):
"""Raised when `git push` with arbitrary args fails."""
def push_args(
args: List[str] | None = None,
*,
cwd: str = ".",
preview: bool = False,
) -> None:
"""
Execute `git push` with caller-provided arguments.
Examples:
[] -> git push
["--force"] -> git push --force
["origin", "main"] -> git push origin main
["-u", "origin", "feature"] -> git push -u origin feature
"""
extra = args or []
try:
run(["push", *extra], cwd=cwd, preview=preview)
except GitRunError as exc:
details = getattr(exc, "output", None) or getattr(exc, "stderr", None) or ""
raise GitPushArgsError(
(
f"Failed to run `git push` with args={extra!r} "
f"in cwd={cwd!r}.\n{details}"
).rstrip(),
cwd=cwd,
) from exc
+1
View File
@@ -0,0 +1 @@
"""GitHub-related Python helpers for pkgmgr."""
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Fail when a hadolint SARIF report contains warnings or errors."""
from __future__ import annotations
import json
import sys
from pathlib import Path
def main() -> int:
sarif_path = Path(sys.argv[1] if len(sys.argv) > 1 else "hadolint-results.sarif")
with sarif_path.open("r", encoding="utf-8") as handle:
sarif = json.load(handle)
results = sarif.get("runs", [{}])[0].get("results", [])
levels = [result.get("level", "") for result in results]
warnings = sum(1 for level in levels if level == "warning")
errors = sum(1 for level in levels if level == "error")
print(f"SARIF results: total={len(results)} warnings={warnings} errors={errors}")
return 1 if warnings + errors > 0 else 0
if __name__ == "__main__":
raise SystemExit(main())