Compare commits

...
14 Commits
Author SHA1 Message Date
Kevin Veen-Birkenbach a6c40451fe Release version 1.13.4
Mark stable commit / mark-stable (push) Canceled after 0s
2026-05-27 20:32:39 +02:00
Kevin Veen-BirkenbachandClaude Opus 4.7 5fa2709a84 feat(release): resolve package name from packaging files, not folder
Previously the release workflow derived the distro-package name from
`os.path.basename(repo_root)`. Renaming the repo folder (e.g.
`infinito-nexus` → `infinito-nexus-core`) silently rewrote
`debian/changelog`'s top entry to the new folder name while
`debian/control` still pinned the legacy `Package:` value. dpkg-source
refuses to build a source package when the two disagree.

Add `resolve_package_name(paths)` that consults the existing packaging
files in priority order (debian/control `Package:` → PKGBUILD
`pkgname=` → RPM `.spec` `Name:`) and only falls back to the folder
basename when no packaging metadata is present. Extend `RepoPaths`
with a `debian_control` slot so `resolve_repo_paths` can discover the
file under both `packaging/debian/control` and the legacy `debian/`
location.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:27:01 +02:00
Kevin Veen-BirkenbachandClaude Opus 4.7 386d8aa2f2 fix(install): use runuser and fail non-root with exit 1
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
`su -` runs through pam_systemd on Manjaro/Arch, creating a new login
session that conflicts with the outer sudo session and detaches the
install from the controlling terminal — making `sudo make install`
appear to end while it keeps running in the background. Replace `su`
calls with `runuser`, which is designed for root-invoked scripts and
skips PAM session management.

Also flips init.sh's non-root branch from `exit 0` (silent success) to
`exit 1` with a clear stderr message, so `make install` correctly fails
when invoked without root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:32:49 +02:00
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
44 changed files with 974 additions and 181 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
+40
View File
@@ -1,3 +1,43 @@
## [1.13.4] - 2026-05-27
* Changed
* pkgmgr release now derives the distro-package name from existing
packaging metadata instead of the repository folder name. The lookup
order is packaging/debian/control Package field, then
packaging/arch/PKGBUILD pkgname value, then RPM spec Name field, then
folder basename as legacy fallback. Renaming a repository folder (for
example infinito-nexus to infinito-nexus-core) no longer silently
flips the debian/changelog top entry and the RPM changelog stanza to
a new identifier. Those keep matching the authoritative Package,
pkgname, or Name value in the packaging files, which is what apt,
pacman, and dnf index against.
Added
* RepoPaths gains a debian_control slot that is discovered alongside
debian_changelog under both packaging/debian (new layout) and debian
(legacy layout).
* pkgmgr.actions.release.package_name.resolve_package_name centralises
the priority chain and is unit-tested under
tests/unit/pkgmgr/actions/release/test_package_name.py.
Fixed
* dpkg-source --before-build no longer fails with the message about
source package having two conflicting values after a repo-folder
rename, because the changelog and control file stay in agreement.
## [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.4";
# 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.4
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')
+42
View File
@@ -1,3 +1,45 @@
package-manager (1.13.4-1) unstable; urgency=medium
* Changed
* pkgmgr release now derives the distro-package name from existing
packaging metadata instead of the repository folder name. The lookup
order is packaging/debian/control Package field, then
packaging/arch/PKGBUILD pkgname value, then RPM spec Name field, then
folder basename as legacy fallback. Renaming a repository folder (for
example infinito-nexus to infinito-nexus-core) no longer silently
flips the debian/changelog top entry and the RPM changelog stanza to
a new identifier. Those keep matching the authoritative Package,
pkgname, or Name value in the packaging files, which is what apt,
pacman, and dnf index against.
Added
* RepoPaths gains a debian_control slot that is discovered alongside
debian_changelog under both packaging/debian (new layout) and debian
(legacy layout).
* pkgmgr.actions.release.package_name.resolve_package_name centralises
the priority chain and is unit-tested under
tests/unit/pkgmgr/actions/release/test_package_name.py.
Fixed
* dpkg-source --before-build no longer fails with the message about
source package having two conflicting values after a repo-folder
rename, because the changelog and control file stay in agreement.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 27 May 2026 20:32:39 +0200
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.
+37 -1
View File
@@ -1,5 +1,5 @@
Name: package-manager Name: package-manager
Version: 1.13.2 Version: 1.13.4
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,42 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/ /usr/lib/package-manager/
%changelog %changelog
* Wed May 27 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.4-1
- Changed
* pkgmgr release now derives the distro-package name from existing
packaging metadata instead of the repository folder name. The lookup
order is packaging/debian/control Package field, then
packaging/arch/PKGBUILD pkgname value, then RPM spec Name field, then
folder basename as legacy fallback. Renaming a repository folder (for
example infinito-nexus to infinito-nexus-core) no longer silently
flips the debian/changelog top entry and the RPM changelog stanza to
a new identifier. Those keep matching the authoritative Package,
pkgname, or Name value in the packaging files, which is what apt,
pacman, and dnf index against.
Added
* RepoPaths gains a debian_control slot that is discovered alongside
debian_changelog under both packaging/debian (new layout) and debian
(legacy layout).
* pkgmgr.actions.release.package_name.resolve_package_name centralises
the priority chain and is unit-tested under
tests/unit/pkgmgr/actions/release/test_package_name.py.
Fixed
* dpkg-source --before-build no longer fails with the message about
source package having two conflicting values after a repo-folder
rename, because the changelog and control file stay in agreement.
* 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.4"
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
@@ -38,7 +38,7 @@ echo "[aur-builder-setup] Configuring sudoers for aur_builder..."
${ROOT_CMD} bash -c "echo '%aur_builder ALL=(ALL) NOPASSWD: /usr/bin/pacman' > /etc/sudoers.d/aur_builder" ${ROOT_CMD} bash -c "echo '%aur_builder ALL=(ALL) NOPASSWD: /usr/bin/pacman' > /etc/sudoers.d/aur_builder"
${ROOT_CMD} chmod 0440 /etc/sudoers.d/aur_builder ${ROOT_CMD} chmod 0440 /etc/sudoers.d/aur_builder
RUN_AS_AUR=(su - aur_builder -s /bin/bash -c) RUN_AS_AUR=(runuser -u aur_builder -- bash -c)
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..." echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
+1 -1
View File
@@ -47,7 +47,7 @@ echo "[arch/package] Using 'aur_builder' user for makepkg..."
chown -R aur_builder:aur_builder "${BUILD_ROOT}" chown -R aur_builder:aur_builder "${BUILD_ROOT}"
echo "[arch/package] Running makepkg in: ${PKG_BUILD_DIR}" echo "[arch/package] Running makepkg in: ${PKG_BUILD_DIR}"
su aur_builder -c "cd '${PKG_BUILD_DIR}' && rm -f package-manager-*.pkg.tar.* && makepkg --noconfirm --clean --nodeps" runuser -u aur_builder -- bash -c "cd '${PKG_BUILD_DIR}' && rm -f package-manager-*.pkg.tar.* && makepkg --noconfirm --clean --nodeps"
echo "[arch/package] Installing generated Arch package..." echo "[arch/package] Installing generated Arch package..."
pkg_path="$(find "${PKG_BUILD_DIR}" -maxdepth 1 -type f -name 'package-manager-*.pkg.tar.*' | head -n1)" pkg_path="$(find "${PKG_BUILD_DIR}" -maxdepth 1 -type f -name 'package-manager-*.pkg.tar.*' | head -n1)"
+2 -2
View File
@@ -2,8 +2,8 @@
set -euo pipefail set -euo pipefail
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "[installation/install] Warning: Installation is just possible via root." echo "[installation/install] ERROR: Installation requires root. Re-run with sudo." >&2
exit 0 exit 1
fi fi
echo "[installation] Running as root (EUID=0)." echo "[installation] Running as root (EUID=0)."
@@ -0,0 +1,75 @@
"""Resolve the distro-package name for a release.
The release flow writes the package identifier into `debian/changelog`,
the RPM `%changelog` stanza, etc. Historically pkgmgr derived this
identifier from the repository folder name (`os.path.basename(repo_root)`),
which silently breaks when the repo is renamed but the existing packaging
files still ship the legacy name. Renaming the folder must not change the
distro-package identity — `apt`, `pacman`, `dnf`, and every downstream
manifest pin the old name.
The resolver therefore walks the existing packaging files in priority
order and only falls back to the folder name when none of them ship an
explicit name.
Priority:
1. `debian/control` `Package:` field (most authoritative — dpkg-source
refuses to build if changelog and control disagree)
2. `packaging/arch/PKGBUILD` `pkgname=` value
3. RPM spec `Name:` field
4. Repository folder basename (legacy fallback)
"""
from __future__ import annotations
import os
import re
from typing import Optional
from pkgmgr.core.repository.paths import RepoPaths
_DEBIAN_PACKAGE_RE = re.compile(r"^Package:\s*(\S+)\s*$", re.MULTILINE)
_PKGBUILD_NAME_RE = re.compile(r"^pkgname=([^\s#]+)\s*$", re.MULTILINE)
_RPM_NAME_RE = re.compile(r"^Name:\s*(\S+)\s*$", re.MULTILINE)
def _read(path: Optional[str]) -> str:
if not path or not os.path.isfile(path):
return ""
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except OSError:
return ""
def _extract(pattern: re.Pattern[str], text: str) -> Optional[str]:
if not text:
return None
match = pattern.search(text)
if not match:
return None
value = match.group(1).strip().strip('"').strip("'")
return value or None
def resolve_package_name(paths: RepoPaths) -> str:
"""Return the distro-package name for the repo, with a folder fallback.
The fallback uses `os.path.basename(paths.repo_dir)` so behaviour is
backwards-compatible for repos that ship no packaging metadata yet.
"""
debian_name = _extract(_DEBIAN_PACKAGE_RE, _read(paths.debian_control))
if debian_name:
return debian_name
pkgbuild_name = _extract(_PKGBUILD_NAME_RE, _read(paths.arch_pkgbuild))
if pkgbuild_name:
return pkgbuild_name
rpm_name = _extract(_RPM_NAME_RE, _read(paths.rpm_spec))
if rpm_name:
return rpm_name
return os.path.basename(paths.repo_dir) or "package"
+2 -1
View File
@@ -24,6 +24,7 @@ from .git_ops import (
is_highest_version_tag, is_highest_version_tag,
update_latest_tag, update_latest_tag,
) )
from .package_name import resolve_package_name
from .prompts import confirm_proceed_release, should_delete_branch from .prompts import confirm_proceed_release, should_delete_branch
from .versioning import bump_semver, determine_current_version from .versioning import bump_semver, determine_current_version
@@ -90,7 +91,7 @@ def _release_impl(
if changelog_message.strip(): if changelog_message.strip():
effective_message = changelog_message.strip() effective_message = changelog_message.strip()
package_name = os.path.basename(repo_root) or "package-manager" package_name = resolve_package_name(paths)
if paths.debian_changelog: if paths.debian_changelog:
update_debian_changelog( update_debian_changelog(
@@ -0,0 +1,89 @@
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)
+85 -26
View File
@@ -1,17 +1,67 @@
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 +69,50 @@ 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
+9
View File
@@ -36,6 +36,7 @@ class RepoPaths:
# Packaging-related files # Packaging-related files
arch_pkgbuild: Optional[str] arch_pkgbuild: Optional[str]
debian_changelog: Optional[str] debian_changelog: Optional[str]
debian_control: Optional[str]
rpm_spec: Optional[str] rpm_spec: Optional[str]
@@ -102,6 +103,13 @@ def resolve_repo_paths(repo_dir: str) -> RepoPaths:
] ]
) )
debian_control = _first_existing(
[
os.path.join(repo_dir, "packaging", "debian", "control"),
os.path.join(repo_dir, "debian", "control"),
]
)
# RPM spec: prefer the canonical file, else first spec in packaging/fedora, else first spec in repo root. # RPM spec: prefer the canonical file, else first spec in packaging/fedora, else first spec in repo root.
rpm_spec = _first_existing( rpm_spec = _first_existing(
[ [
@@ -122,5 +130,6 @@ def resolve_repo_paths(repo_dir: str) -> RepoPaths:
changelog_md=changelog_md, changelog_md=changelog_md,
arch_pkgbuild=arch_pkgbuild, arch_pkgbuild=arch_pkgbuild,
debian_changelog=debian_changelog, debian_changelog=debian_changelog,
debian_control=debian_control,
rpm_spec=rpm_spec, rpm_spec=rpm_spec,
) )
+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())
@@ -0,0 +1,110 @@
"""Unit tests for `pkgmgr.actions.release.package_name.resolve_package_name`.
The resolver must prefer the explicit name from existing packaging files
over the repository folder name so that renaming the folder does not
silently rename the distro package.
"""
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from typing import Optional
from pkgmgr.actions.release.package_name import resolve_package_name
from pkgmgr.core.repository.paths import RepoPaths
def _paths(
repo_dir: str,
*,
debian_control: Optional[str] = None,
arch_pkgbuild: Optional[str] = None,
rpm_spec: Optional[str] = None,
) -> RepoPaths:
return RepoPaths(
repo_dir=repo_dir,
pyproject_toml=os.path.join(repo_dir, "pyproject.toml"),
flake_nix=os.path.join(repo_dir, "flake.nix"),
changelog_md=None,
arch_pkgbuild=arch_pkgbuild,
debian_changelog=None,
debian_control=debian_control,
rpm_spec=rpm_spec,
)
class TestResolvePackageName(unittest.TestCase):
def test_debian_control_wins_over_folder(self) -> None:
with tempfile.TemporaryDirectory(prefix="infinito-nexus-core_") as repo:
control = Path(repo) / "control"
control.write_text(
"Source: infinito-nexus\nPackage: infinito-nexus\n",
encoding="utf-8",
)
self.assertEqual(
resolve_package_name(_paths(repo, debian_control=str(control))),
"infinito-nexus",
)
def test_pkgbuild_used_when_no_control(self) -> None:
with tempfile.TemporaryDirectory(prefix="infinito-nexus-core_") as repo:
pkgbuild = Path(repo) / "PKGBUILD"
pkgbuild.write_text(
"pkgname=infinito-nexus\npkgver=1.0\n", encoding="utf-8"
)
self.assertEqual(
resolve_package_name(_paths(repo, arch_pkgbuild=str(pkgbuild))),
"infinito-nexus",
)
def test_rpm_spec_used_when_no_control_no_pkgbuild(self) -> None:
with tempfile.TemporaryDirectory(prefix="infinito-nexus-core_") as repo:
spec = Path(repo) / "pkg.spec"
spec.write_text("Name: infinito-nexus\n", encoding="utf-8")
self.assertEqual(
resolve_package_name(_paths(repo, rpm_spec=str(spec))),
"infinito-nexus",
)
def test_folder_fallback_when_no_packaging_metadata(self) -> None:
with tempfile.TemporaryDirectory(prefix="solo-tool_") as repo:
self.assertEqual(
resolve_package_name(_paths(repo)),
os.path.basename(repo),
)
def test_priority_debian_over_pkgbuild_over_spec(self) -> None:
with tempfile.TemporaryDirectory() as repo:
control = Path(repo) / "control"
control.write_text("Package: deb-name\n", encoding="utf-8")
pkgbuild = Path(repo) / "PKGBUILD"
pkgbuild.write_text("pkgname=arch-name\n", encoding="utf-8")
spec = Path(repo) / "x.spec"
spec.write_text("Name: rpm-name\n", encoding="utf-8")
self.assertEqual(
resolve_package_name(
_paths(
repo,
debian_control=str(control),
arch_pkgbuild=str(pkgbuild),
rpm_spec=str(spec),
)
),
"deb-name",
)
def test_strips_quotes_in_pkgbuild(self) -> None:
with tempfile.TemporaryDirectory() as repo:
pkgbuild = Path(repo) / "PKGBUILD"
pkgbuild.write_text("pkgname='quoted-name'\n", encoding="utf-8")
self.assertEqual(
resolve_package_name(_paths(repo, arch_pkgbuild=str(pkgbuild))),
"quoted-name",
)
if __name__ == "__main__":
unittest.main()