Compare commits

...

30 Commits

Author SHA1 Message Date
607102e7f8 chore(claude): add project settings with sandbox and ask rules
Some checks failed
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
133cf63b9f Release version 1.13.3 2026-03-26 17:10:21 +01:00
6334936e8a fix(ci): resolve workflow and docker scan findings 2026-03-26 16:44:02 +01:00
946965f016 fix(ci): grant reusable workflows security permissions 2026-03-26 16:33:40 +01:00
541a7f679f feat(ci): add docker lint and codeql workflows 2026-03-26 16:30:36 +01:00
128f71745a refactor(ci): organize workflow scripts and gate publish on main 2026-03-26 15:58:18 +01:00
df2ce636c8 fix(ci): make mark-stable main-only and cancel stale runs 2026-03-26 14:57:04 +01:00
3b0dabf2a7 Release version 1.13.2 2026-03-26 12:26:55 +01:00
697370c906 Merge branch 'fix/nix-centos' 2026-03-26 12:26:26 +01:00
bc57172d92 fix(nix): fail fast when bootstrap is unavailable 2026-03-26 07:56:55 +01:00
0e7e23dce5 Release version 1.13.1 2026-03-20 02:57:25 +01:00
9d53f4c6f5 Fix GPG verification runtime handling 2026-03-20 02:51:51 +01:00
a46d85b541 Release version 1.13.0 2026-03-20 01:29:38 +01:00
acaea11eb6 Set CentOS image to latest 2026-03-20 01:28:49 +01:00
056d21a859 Release version 1.12.5 2026-02-24 09:35:39 +01:00
612ba5069d Increase stable gate wait time to 2 hours 2026-02-24 09:34:45 +01:00
551e245218 Release version 1.12.4 2026-02-24 09:32:01 +01:00
814523eac2 Gate stable tag updates on successful main CI 2026-02-24 09:30:24 +01:00
4f2c5013a7 Release version 1.12.3 2026-02-24 08:29:34 +01:00
e01bb8c39a nix: pin flake input to nixos-25.11 and track flake.lock 2026-02-24 08:23:33 +01:00
461a3c334d Release version 1.12.2 2026-02-24 07:40:55 +01:00
e3de46c6a4 Removed infinito-sphinx from package manager, because it's managed now via docker in infinito.nexus 2026-02-24 07:40:01 +01:00
b20882f492 Release version 1.12.1
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2026-02-14 23:26:17 +01:00
430f21735e fix(nix): prefer distro nix binaries over PATH lookup 2026-02-14 23:23:16 +01:00
acf1b69b70 Release version 1.12.0 2026-02-08 18:26:25 +01:00
7d574e67ec Add concurrency groups to CI and mark-stable workflows
Introduce explicit concurrency settings to the CI and mark-stable
workflows to serialize runs per repository and ref. This prevents
overlapping executions for the same branch or tag and makes pipeline
behavior more predictable during rapid pushes.

https://chatgpt.com/share/6988bef0-1a0c-800f-93df-7a6c1bdc0331
2026-02-08 18:25:31 +01:00
aad6814fc5 Release version 1.11.2 2026-02-08 18:21:50 +01:00
411cd2df66 Remove tag trigger from mark-stable workflow
Stop running the mark-stable workflow on v* tag pushes so it executes
only on branch updates. This prevents duplicate or unintended runs
after version tags are created as part of the release process.

https://chatgpt.com/share/6988bef0-1a0c-800f-93df-7a6c1bdc0331
2026-02-08 18:20:48 +01:00
849d29c044 Release version 1.11.1 2026-02-08 18:18:09 +01:00
0947dea01e Fix release push to send branch and version tag together
Push master and the newly created version tag in a single git push command
so the CI release workflow can detect the tag on HEAD. This aligns the
release script with the new master-based release pipeline and prevents
missed automated releases caused by separate branch and tag pushes.

https://chatgpt.com/share/6988bef0-1a0c-800f-93df-7a6c1bdc0331
2026-02-08 17:51:15 +01:00
49 changed files with 800 additions and 188 deletions

12
.claude/settings.json Normal file
View File

@@ -0,0 +1,12 @@
{
"permissions": {
"ask": [
"Skill(update-config)",
"Skill(update-config:*)"
]
},
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true
}
}

View File

@@ -2,34 +2,72 @@ name: CI
on:
push:
branches-ignore:
- main
branches:
- '**'
pull_request:
permissions:
contents: read
concurrency:
group: global-ci-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
security-codeql:
permissions:
contents: read
packages: read
security-events: write
uses: ./.github/workflows/security-codeql.yml
test-unit:
permissions:
contents: read
uses: ./.github/workflows/test-unit.yml
test-integration:
permissions:
contents: read
uses: ./.github/workflows/test-integration.yml
test-env-virtual:
permissions:
contents: read
uses: ./.github/workflows/test-env-virtual.yml
test-env-nix:
permissions:
contents: read
uses: ./.github/workflows/test-env-nix.yml
test-e2e:
permissions:
contents: read
uses: ./.github/workflows/test-e2e.yml
test-virgin-user:
permissions:
contents: read
uses: ./.github/workflows/test-virgin-user.yml
test-virgin-root:
permissions:
contents: read
uses: ./.github/workflows/test-virgin-root.yml
lint-shell:
permissions:
contents: read
uses: ./.github/workflows/lint-shell.yml
lint-python:
permissions:
contents: read
uses: ./.github/workflows/lint-python.yml
lint-docker:
permissions:
contents: read
security-events: write
uses: ./.github/workflows/lint-docker.yml

40
.github/workflows/lint-docker.yml vendored Normal file
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

View File

@@ -3,6 +3,9 @@ name: Ruff (Python code sniffer)
on:
workflow_call:
permissions:
contents: read
jobs:
lint-python:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: ShellCheck
on:
workflow_call:
permissions:
contents: read
jobs:
lint-shell:
runs-on: ubuntu-latest

View File

@@ -1,110 +1,39 @@
name: Mark stable commit
concurrency:
group: mark-stable-${{ github.repository }}-main
cancel-in-progress: true
on:
push:
branches:
- main # still run tests for main
tags:
- 'v*' # run tests for version tags (e.g. v0.9.1)
- 'v*'
jobs:
test-unit:
uses: ./.github/workflows/test-unit.yml
test-integration:
uses: ./.github/workflows/test-integration.yml
test-env-virtual:
uses: ./.github/workflows/test-env-virtual.yml
test-env-nix:
uses: ./.github/workflows/test-env-nix.yml
test-e2e:
uses: ./.github/workflows/test-e2e.yml
test-virgin-user:
uses: ./.github/workflows/test-virgin-user.yml
test-virgin-root:
uses: ./.github/workflows/test-virgin-root.yml
lint-shell:
uses: ./.github/workflows/lint-shell.yml
lint-python:
uses: ./.github/workflows/lint-python.yml
mark-stable:
needs:
- lint-shell
- lint-python
- test-unit
- test-integration
- test-env-nix
- test-env-virtual
- test-e2e
- test-virgin-user
- test-virgin-root
runs-on: ubuntu-latest
# Only run this job if the push is for a version tag (v*)
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 330
permissions:
contents: write # Required to move/update the tag
actions: read
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
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
run: |
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}"
# 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}."
if: steps.branch-check.outputs.is_on_main == 'true'
run: bash scripts/github/mark-stable/mark-stable-if-highest-version.sh

View File

@@ -21,44 +21,30 @@ jobs:
fetch-depth: 0
- name: Checkout workflow_run commit and refresh tags
run: |
set -euo pipefail
git checkout -f "${{ github.event.workflow_run.head_sha }}"
git fetch --tags --force
git tag --list 'stable' 'v*' --sort=version:refname | tail -n 20
env:
WORKFLOW_RUN_SHA: ${{ github.event.workflow_run.head_sha }}
run: bash scripts/github/publish-containers/checkout-workflow-run-commit.sh
- 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
id: info
run: |
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" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "is_stable=${IS_STABLE}" >> "$GITHUB_OUTPUT"
if: steps.branch-check.outputs.is_on_main == 'true'
run: bash scripts/github/publish-containers/compute-publish-container-info.sh
- name: Set up Docker Buildx
if: ${{ steps.info.outputs.should_publish == 'true' }}
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
with:
use: true
- name: Login to GHCR
if: ${{ steps.info.outputs.should_publish == 'true' }}
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -66,9 +52,8 @@ jobs:
- name: Publish all images
if: ${{ steps.info.outputs.should_publish == 'true' }}
run: |
set -euo pipefail
OWNER="${{ github.repository_owner }}" \
VERSION="${{ steps.info.outputs.version }}" \
IS_STABLE="${{ steps.info.outputs.is_stable }}" \
bash scripts/build/publish.sh
env:
OWNER: ${{ github.repository_owner }}
VERSION: ${{ steps.info.outputs.version }}
IS_STABLE: ${{ steps.info.outputs.is_stable }}
run: bash scripts/github/publish-containers/publish-container-images.sh

47
.github/workflows/security-codeql.yml vendored Normal file
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 }}"

View File

@@ -3,6 +3,9 @@ name: Test End-To-End
on:
workflow_call:
permissions:
contents: read
jobs:
test-e2e:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: Test Virgin Nix (flake only)
on:
workflow_call:
permissions:
contents: read
jobs:
test-env-nix:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: Test OS Containers
on:
workflow_call:
permissions:
contents: read
jobs:
test-env-virtual:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: Test Code Integration
on:
workflow_call:
permissions:
contents: read
jobs:
test-integration:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: Test Units
on:
workflow_call:
permissions:
contents: read
jobs:
test-unit:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: Test Virgin Root
on:
workflow_call:
permissions:
contents: read
jobs:
test-virgin-root:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: Test Virgin User
on:
workflow_call:
permissions:
contents: read
jobs:
test-virgin-user:
runs-on: ubuntu-latest

3
.gitignore vendored
View File

@@ -24,10 +24,9 @@ package-manager-*
.DS_Store
Thumbs.db
# Nix Cache to speed up tests
# Nix cache to speed up tests
.nix/
.nix-dev-installed
flake.lock
# Ignore logs
*.log

View File

@@ -1,3 +1,67 @@
## [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
* Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path.
## [1.13.1] - 2026-03-20
* Fixed misleading GPG verification failures by adding explicit git and gnupg runtime dependencies and surfacing signing-key lookup errors accurately.
## [1.13.0] - 2026-03-20
* Set CentOS docker image to latest
## [1.12.5] - 2026-02-24
* The stable-tag workflow now waits up to two hours for a successful main-branch CI run on the same commit before updating stable.
## [1.12.4] - 2026-02-24
* The release pipeline now updates the stable tag only for v* tags after a successful CI run on main for the same commit, while avoiding duplicate test executions.
## [1.12.3] - 2026-02-24
* Stabilized Nix-based builds by switching to nixos-25.11 and committing flake.lock, ensuring reproducible pkgmgr test/runtime environments (with pip) and avoiding transient sphinx/Python 3.11 breakage.
## [1.12.2] - 2026-02-24
* Removed infinito-sphinx package
## [1.12.1] - 2026-02-14
* pkgmgr now prefers distro-managed nix binaries on Arch before profile/PATH resolution, preventing libllhttp mismatch failures after pacman system upgrades.
## [1.12.0] - 2026-02-08
* Adds explicit concurrency groups to the CI and mark-stable workflows to prevent overlapping runs on the same branch and make pipeline execution more predictable.
## [1.11.2] - 2026-02-08
* Removes the v* tag trigger from the mark-stable workflow so it runs only on branch pushes and avoids duplicate executions during releases.
## [1.11.1] - 2026-02-08
* Implements pushing the branch and the version tag together in a single command so the CI release workflow can reliably detect the version tag on HEAD.
## [1.11.0] - 2026-01-21
* Adds a dedicated slim Docker image for pkgmgr and publishes slim variants for all supported distros.

View File

@@ -43,10 +43,10 @@ WORKDIR /build
COPY . .
# Build and install distro-native package-manager package
RUN set -euo pipefail; \
RUN set -eu; \
echo "Building and installing package-manager via make install..."; \
make install; \
cd /; rm -rf /build
rm -rf /build
# Entry point
COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh
@@ -64,5 +64,4 @@ CMD ["pkgmgr", "--help"]
FROM full AS slim
COPY scripts/docker/slim.sh /usr/local/bin/slim.sh
RUN chmod +x /usr/local/bin/slim.sh
RUN /usr/local/bin/slim.sh
RUN chmod +x /usr/local/bin/slim.sh && /usr/local/bin/slim.sh

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1771714954,
"narHash": "sha256-nhZJPnBavtu40/L2aqpljrfUNb2rxmWTmSjK2c9UKds=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "afbbf774e2087c3d734266c22f96fca2e78d3620",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -6,7 +6,7 @@
};
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
};
outputs = { self, nixpkgs }:
@@ -32,7 +32,7 @@
rec {
pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager";
version = "1.11.0";
version = "1.13.3";
# Use the git repo as source
src = ./.;
@@ -51,6 +51,8 @@
pyPkgs.pyyaml
pyPkgs.jinja2
pyPkgs.pip
pkgs.git
pkgs.gnupg
];
doCheck = false;
@@ -87,6 +89,7 @@
buildInputs = [
pythonWithDeps
pkgs.git
pkgs.gnupg
ansiblePkg
];

View File

@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager
pkgver=1.11.0
pkgver=1.13.3
pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any')

View File

@@ -1,9 +1,9 @@
post_install() {
/usr/lib/package-manager/nix/init.sh || echo ">>> ERROR: /usr/lib/package-manager/nix/init.sh not found or not executable."
/usr/lib/package-manager/nix/init.sh
}
post_upgrade() {
/usr/lib/package-manager/nix/init.sh || echo ">>> ERROR: /usr/lib/package-manager/nix/init.sh not found or not executable."
/usr/lib/package-manager/nix/init.sh
}
post_remove() {

View File

@@ -1,3 +1,79 @@
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
* Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path.
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 26 Mar 2026 12:26:55 +0100
package-manager (1.13.1-1) unstable; urgency=medium
* Fixed misleading GPG verification failures by adding explicit git and gnupg runtime dependencies and surfacing signing-key lookup errors accurately.
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 20 Mar 2026 02:57:25 +0100
package-manager (1.13.0-1) unstable; urgency=medium
* Set CentOS docker image to latest
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 20 Mar 2026 01:29:38 +0100
package-manager (1.12.5-1) unstable; urgency=medium
* The stable-tag workflow now waits up to two hours for a successful main-branch CI run on the same commit before updating stable.
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 24 Feb 2026 09:35:39 +0100
package-manager (1.12.4-1) unstable; urgency=medium
* The release pipeline now updates the stable tag only for v* tags after a successful CI run on main for the same commit, while avoiding duplicate test executions.
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 24 Feb 2026 09:32:01 +0100
package-manager (1.12.3-1) unstable; urgency=medium
* Stabilized Nix-based builds by switching to nixos-25.11 and committing flake.lock, ensuring reproducible pkgmgr test/runtime environments (with pip) and avoiding transient sphinx/Python 3.11 breakage.
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 24 Feb 2026 08:29:34 +0100
package-manager (1.12.2-1) unstable; urgency=medium
* Removed infinito-sphinx package
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 24 Feb 2026 07:40:55 +0100
package-manager (1.12.1-1) unstable; urgency=medium
* pkgmgr now prefers distro-managed nix binaries on Arch before profile/PATH resolution, preventing libllhttp mismatch failures after pacman system upgrades.
-- Kevin Veen-Birkenbach <kevin@veen.world> Sat, 14 Feb 2026 23:26:17 +0100
package-manager (1.12.0-1) unstable; urgency=medium
* Adds explicit concurrency groups to the CI and mark-stable workflows to prevent overlapping runs on the same branch and make pipeline execution more predictable.
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 08 Feb 2026 18:26:25 +0100
package-manager (1.11.2-1) unstable; urgency=medium
* Removes the v* tag trigger from the mark-stable workflow so it runs only on branch pushes and avoids duplicate executions during releases.
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 08 Feb 2026 18:21:50 +0100
package-manager (1.11.1-1) unstable; urgency=medium
* Implements pushing the branch and the version tag together in a single command so the CI release workflow can reliably detect the version tag on HEAD.
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 08 Feb 2026 18:18:09 +0100
package-manager (1.11.0-1) unstable; urgency=medium
* Adds a dedicated slim Docker image for pkgmgr and publishes slim variants for all supported distros.

View File

@@ -3,7 +3,7 @@ set -e
case "$1" in
configure)
/usr/lib/package-manager/nix/init.sh || echo ">>> ERROR: /usr/lib/package-manager/nix/init.sh not found or not executable."
/usr/lib/package-manager/nix/init.sh
;;
esac

View File

@@ -1,5 +1,5 @@
Name: package-manager
Version: 1.11.0
Version: 1.13.3
Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -62,7 +62,7 @@ rm -rf \
%{buildroot}/usr/lib/package-manager/.gitkeep || true
%post
/usr/lib/package-manager/nix/init.sh || echo ">>> ERROR: /usr/lib/package-manager/nix/init.sh not found or not executable."
/usr/lib/package-manager/nix/init.sh
%postun
echo ">>> package-manager removed. Nix itself was not removed."
@@ -74,6 +74,46 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/
%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
- Fail fast with a clear error when the Nix bootstrap or nix binary is unavailable instead of continuing with a broken startup path.
* Fri Mar 20 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.1-1
- Fixed misleading GPG verification failures by adding explicit git and gnupg runtime dependencies and surfacing signing-key lookup errors accurately.
* Fri Mar 20 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.0-1
- Set CentOS docker image to latest
* Tue Feb 24 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.12.5-1
- The stable-tag workflow now waits up to two hours for a successful main-branch CI run on the same commit before updating stable.
* Tue Feb 24 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.12.4-1
- The release pipeline now updates the stable tag only for v* tags after a successful CI run on main for the same commit, while avoiding duplicate test executions.
* Tue Feb 24 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.12.3-1
- Stabilized Nix-based builds by switching to nixos-25.11 and committing flake.lock, ensuring reproducible pkgmgr test/runtime environments (with pip) and avoiding transient sphinx/Python 3.11 breakage.
* Tue Feb 24 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.12.2-1
- Removed infinito-sphinx package
* Sat Feb 14 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.12.1-1
- pkgmgr now prefers distro-managed nix binaries on Arch before profile/PATH resolution, preventing libllhttp mismatch failures after pacman system upgrades.
* Sun Feb 08 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.12.0-1
- Adds explicit concurrency groups to the CI and mark-stable workflows to prevent overlapping runs on the same branch and make pipeline execution more predictable.
* Sun Feb 08 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.11.2-1
- Removes the v* tag trigger from the mark-stable workflow so it runs only on branch pushes and avoids duplicate executions during releases.
* Sun Feb 08 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.11.1-1
- Implements pushing the branch and the version tag together in a single command so the CI release workflow can reliably detect the version tag on HEAD.
* Wed Jan 21 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.11.0-1
- Adds a dedicated slim Docker image for pkgmgr and publishes slim variants for all supported distros.

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "kpmx"
version = "1.11.0"
version = "1.13.3"
description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md"
requires-python = ">=3.9"

View File

@@ -5,7 +5,7 @@ set -euo pipefail
: "${BASE_IMAGE_DEBIAN:=debian:stable-slim}"
: "${BASE_IMAGE_UBUNTU:=ubuntu:latest}"
: "${BASE_IMAGE_FEDORA:=fedora:latest}"
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}"
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:latest}"
resolve_base_image() {
local PKGMGR_DISTRO="$1"

View File

@@ -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

View File

@@ -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}."

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -16,6 +16,7 @@ fi
pacman -S --noconfirm --needed \
base-devel \
git \
gnupg \
rsync \
curl \
ca-certificates \

View File

@@ -6,6 +6,7 @@ echo "[centos/dependencies] Installing CentOS build dependencies..."
dnf -y update
dnf -y install \
git \
gnupg2 \
rsync \
rpm-build \
make \

View File

@@ -9,6 +9,7 @@ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
debhelper \
dpkg-dev \
git \
gnupg \
rsync \
bash \
curl \

View File

@@ -6,6 +6,7 @@ echo "[fedora/dependencies] Installing Fedora build dependencies..."
dnf -y update
dnf -y install \
git \
gnupg2 \
rsync \
rpm-build \
make \

View File

@@ -9,6 +9,7 @@ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
debhelper \
dpkg-dev \
git \
gnupg \
tzdata \
lsb-release \
rsync \

View File

@@ -37,10 +37,16 @@ fi
# ---------------------------------------------------------------------------
if ! command -v nix >/dev/null 2>&1; then
if [[ -x "${FLAKE_DIR}/nix/init.sh" ]]; then
"${FLAKE_DIR}/nix/init.sh" || true
"${FLAKE_DIR}/nix/init.sh"
fi
fi
if ! command -v nix >/dev/null 2>&1; then
echo "[launcher] ERROR: 'nix' binary not found on PATH after init." >&2
echo "[launcher] Nix is required to run pkgmgr (no Python fallback)." >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Primary path: use Nix flake if available (with GitHub 403 retry)
# ---------------------------------------------------------------------------
@@ -51,7 +57,3 @@ if declare -F run_with_github_403_retry >/dev/null; then
else
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
fi
echo "[launcher] ERROR: 'nix' binary not found on PATH after init."
echo "[launcher] Nix is required to run pkgmgr (no Python fallback)."
exit 1

View File

@@ -36,16 +36,17 @@ real_exe() {
# Resolve nix binary path robustly (works across distros + Arch /usr/sbin)
resolve_nix_bin() {
local nix_cmd=""
nix_cmd="$(command -v nix 2>/dev/null || true)"
[[ -n "$nix_cmd" ]] && real_exe "$nix_cmd" && return 0
# IMPORTANT: prefer system locations before /usr/local to avoid self-symlink traps
# IMPORTANT: prefer distro-managed locations first.
# This avoids pinning /usr/local/bin/nix to a stale user-profile nix binary.
[[ -x /usr/sbin/nix ]] && { echo "/usr/sbin/nix"; return 0; } # Arch package can land here
[[ -x /usr/bin/nix ]] && { echo "/usr/bin/nix"; return 0; }
[[ -x /bin/nix ]] && { echo "/bin/nix"; return 0; }
# /usr/local last, and only if it resolves to a real executable
local nix_cmd=""
nix_cmd="$(command -v nix 2>/dev/null || true)"
[[ -n "$nix_cmd" ]] && real_exe "$nix_cmd" && return 0
# /usr/local after system locations, and only if it resolves to a real executable
[[ -e /usr/local/bin/nix ]] && real_exe "/usr/local/bin/nix" && return 0
[[ -x /nix/var/nix/profiles/default/bin/nix ]] && {

View File

@@ -5,8 +5,8 @@ import sys
from typing import Optional
from pkgmgr.actions.branch import close_branch
from pkgmgr.core.git import GitRunError
from pkgmgr.core.git.commands import add, commit, push, tag_annotated
from pkgmgr.core.git import GitRunError, run
from pkgmgr.core.git.commands import add, commit, tag_annotated
from pkgmgr.core.git.queries import get_current_branch
from pkgmgr.core.repository.paths import resolve_repo_paths
@@ -133,8 +133,7 @@ def _release_impl(
add(existing_files, preview=True)
commit(commit_msg, all=True, preview=True)
tag_annotated(new_tag, tag_msg, preview=True)
push("origin", branch, preview=True)
push("origin", new_tag, preview=True)
run(["push", "origin", branch, new_tag], preview=True)
if is_highest_version_tag(new_tag):
update_latest_tag(new_tag, preview=True)
@@ -156,9 +155,8 @@ def _release_impl(
commit(commit_msg, all=True, preview=False)
tag_annotated(new_tag, tag_msg, preview=False)
# Push branch and ONLY the newly created version tag (no --tags)
push("origin", branch, preview=False)
push("origin", new_tag, preview=False)
# Push branch and ONLY the newly created version tag in one command (no --tags)
run(["push", "origin", branch, new_tag], preview=False)
# Update 'latest' only if this is the highest version tag
try:

View File

@@ -369,17 +369,6 @@ repositories:
- 44D8F11FD62F878E
- B5690EEEBB952194
- account: kevinveenbirkenbach
alias: infinito-sphinx
description: Contains the logic and configuration for generating documentation using Sphinx for Infinito.Nexus.
homepage: https://github.com/kevinveenbirkenbach/infinito-sphinx
provider: github.com
repository: infinito-sphinx
verified:
gpg_keys:
- 44D8F11FD62F878E
- B5690EEEBB952194
- account: kevinveenbirkenbach
description: A lightweight Python utility to generate dynamic color schemes from a single base color. Provides HSL-based color transformations for theming, UI design, and CSS variable generation. Optimized for integration in Python projects, Flask applications, and Ansible roles.
homepage: https://github.com/kevinveenbirkenbach/colorscheme-generator

View File

@@ -1,13 +1,33 @@
from __future__ import annotations
from ..errors import GitQueryError, GitRunError
from ..run import run
import subprocess
from ..errors import GitNotRepositoryError, GitQueryError
class GitLatestSigningKeyQueryError(GitQueryError):
"""Raised when querying the latest commit signing key fails."""
def _is_not_repository(stderr: str) -> bool:
return "not a git repository" in (stderr or "").lower()
def _looks_like_gpg_runtime_error(stderr: str) -> bool:
lowered = (stderr or "").lower()
markers = (
"cannot run gpg",
"can't check signature",
"no public key",
"failed to create temporary file",
"can't connect to the keyboxd",
"error opening key db",
"gpg failed",
"no such file or directory",
)
return any(marker in lowered for marker in markers)
def get_latest_signing_key(*, cwd: str = ".") -> str:
"""
Return the GPG signing key ID of the latest commit, via:
@@ -17,9 +37,46 @@ def get_latest_signing_key(*, cwd: str = ".") -> str:
Returns:
The key id string (may be empty if commit is not signed).
"""
cmd = ["git", "log", "-1", "--format=%GK"]
try:
return run(["log", "-1", "--format=%GK"], cwd=cwd).strip()
except GitRunError as exc:
result = subprocess.run(
cmd,
cwd=cwd,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except OSError as exc:
raise GitLatestSigningKeyQueryError(
"Failed to query latest signing key.",
"Failed to query latest signing key.\n"
f"Command: {' '.join(cmd)}\n"
f"Reason: {exc}"
) from exc
stdout = (result.stdout or "").strip()
stderr = (result.stderr or "").strip()
if result.returncode != 0:
if _is_not_repository(stderr):
raise GitNotRepositoryError(
f"Not a git repository: {cwd!r}\n"
f"Command: {' '.join(cmd)}\n"
f"STDERR:\n{stderr}"
)
raise GitLatestSigningKeyQueryError(
"Failed to query latest signing key.\n"
f"Command: {' '.join(cmd)}\n"
f"Exit code: {result.returncode}\n"
f"STDOUT:\n{stdout}\n"
f"STDERR:\n{stderr}"
)
if not stdout and stderr and _looks_like_gpg_runtime_error(stderr):
raise GitLatestSigningKeyQueryError(
"Failed to query latest signing key.\n"
f"Command: {' '.join(cmd)}\n"
f"STDERR:\n{stderr}"
)
return stdout

View File

@@ -16,6 +16,7 @@ def verify_repository(repo, repo_dir, mode="local", no_verification=False):
commit_hash = ""
signing_key = ""
signing_key_query_failed = False
# best-effort info collection
try:
@@ -59,6 +60,7 @@ def verify_repository(repo, repo_dir, mode="local", no_verification=False):
except GitLatestSigningKeyQueryError as exc:
error_details.append(str(exc))
signing_key = ""
signing_key_query_failed = True
commit_check_passed = True
gpg_check_passed = True
@@ -78,9 +80,10 @@ def verify_repository(repo, repo_dir, mode="local", no_verification=False):
if expected_gpg_keys:
if not signing_key:
gpg_check_passed = False
error_details.append(
f"Expected one of GPG keys: {expected_gpg_keys}, but no signing key was found."
)
if not signing_key_query_failed:
error_details.append(
f"Expected one of GPG keys: {expected_gpg_keys}, but no signing key was found."
)
elif signing_key not in expected_gpg_keys:
gpg_check_passed = False
error_details.append(

View File

@@ -0,0 +1 @@
"""GitHub-related Python helpers for pkgmgr."""

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())

View File

@@ -0,0 +1,57 @@
from __future__ import annotations
import re
import unittest
from pathlib import Path
def _find_repo_root() -> Path:
here = Path(__file__).resolve()
for parent in here.parents:
if (parent / "pyproject.toml").is_file() and (
parent / "src" / "pkgmgr"
).is_dir():
return parent
raise RuntimeError(
"Could not determine repository root for pkgmgr integration test"
)
class TestGitVerificationRuntimeDependencies(unittest.TestCase):
def test_flake_app_includes_git_and_gpg_runtime_tools(self) -> None:
repo_root = _find_repo_root()
flake_text = (repo_root / "flake.nix").read_text(encoding="utf-8")
self.assertIn("pkgs.git", flake_text)
self.assertIn("pkgs.gnupg", flake_text)
def test_distro_dependency_scripts_install_gpg_tools(self) -> None:
repo_root = _find_repo_root()
expected_packages = {
"arch": "gnupg",
"debian": "gnupg",
"ubuntu": "gnupg",
"fedora": "gnupg2",
"centos": "gnupg2",
}
missing: list[str] = []
for distro, package_name in expected_packages.items():
script_path = (
repo_root / "scripts" / "installation" / distro / "dependencies.sh"
)
content = script_path.read_text(encoding="utf-8")
if not re.search(rf"\b{re.escape(package_name)}\b", content):
missing.append(
f"{distro}: expected package {package_name} in {script_path}"
)
if missing:
self.fail(
"Git signature verification runtime dependencies are incomplete:\n"
+ "\n".join(f" - {item}" for item in missing)
)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,7 +1,8 @@
import unittest
import subprocess
from unittest.mock import patch
from pkgmgr.core.git.errors import GitNotRepositoryError, GitRunError
from pkgmgr.core.git.errors import GitNotRepositoryError
from pkgmgr.core.git.queries.get_latest_signing_key import (
GitLatestSigningKeyQueryError,
get_latest_signing_key,
@@ -10,25 +11,53 @@ from pkgmgr.core.git.queries.get_latest_signing_key import (
class TestGetLatestSigningKey(unittest.TestCase):
@patch(
"pkgmgr.core.git.queries.get_latest_signing_key.run",
return_value="ABCDEF1234567890\n",
"pkgmgr.core.git.queries.get_latest_signing_key.subprocess.run",
return_value=subprocess.CompletedProcess(
args=["git", "log", "-1", "--format=%GK"],
returncode=0,
stdout="ABCDEF1234567890\n",
stderr="",
),
)
def test_strips_output(self, _mock_run) -> None:
out = get_latest_signing_key(cwd="/tmp/repo")
self.assertEqual(out, "ABCDEF1234567890")
@patch(
"pkgmgr.core.git.queries.get_latest_signing_key.run",
side_effect=GitRunError("boom"),
"pkgmgr.core.git.queries.get_latest_signing_key.subprocess.run",
return_value=subprocess.CompletedProcess(
args=["git", "log", "-1", "--format=%GK"],
returncode=1,
stdout="",
stderr="boom",
),
)
def test_wraps_git_run_error(self, _mock_run) -> None:
with self.assertRaises(GitLatestSigningKeyQueryError):
with self.assertRaisesRegex(GitLatestSigningKeyQueryError, "boom"):
get_latest_signing_key(cwd="/tmp/repo")
@patch(
"pkgmgr.core.git.queries.get_latest_signing_key.run",
side_effect=GitNotRepositoryError("no repo"),
"pkgmgr.core.git.queries.get_latest_signing_key.subprocess.run",
return_value=subprocess.CompletedProcess(
args=["git", "log", "-1", "--format=%GK"],
returncode=128,
stdout="",
stderr="fatal: not a git repository",
),
)
def test_does_not_catch_not_repository_error(self, _mock_run) -> None:
with self.assertRaises(GitNotRepositoryError):
get_latest_signing_key(cwd="/tmp/no-repo")
@patch(
"pkgmgr.core.git.queries.get_latest_signing_key.subprocess.run",
return_value=subprocess.CompletedProcess(
args=["git", "log", "-1", "--format=%GK"],
returncode=0,
stdout="",
stderr="error: cannot run gpg: No such file or directory",
),
)
def test_raises_when_git_reports_gpg_runtime_error(self, _mock_run) -> None:
with self.assertRaisesRegex(GitLatestSigningKeyQueryError, "cannot run gpg"):
get_latest_signing_key(cwd="/tmp/repo")

View File

@@ -77,6 +77,23 @@ class TestVerifyRepository(unittest.TestCase):
self.assertEqual(commit, "")
self.assertEqual(key, "")
def test_verified_gpg_query_error_does_not_add_missing_key_fallback(self) -> None:
repo = {"verified": {"commit": None, "gpg_keys": ["ABC"]}}
with (
patch("pkgmgr.core.repository.verify.get_head_commit", return_value=""),
patch(
"pkgmgr.core.repository.verify.get_latest_signing_key",
side_effect=GitLatestSigningKeyQueryError("cannot run gpg"),
),
):
ok, errors, commit, key = verify_repository(repo, "/tmp/repo", mode="local")
self.assertFalse(ok)
self.assertIn("cannot run gpg", " ".join(errors))
self.assertFalse(any("no signing key was found" in e for e in errors))
self.assertEqual(commit, "")
self.assertEqual(key, "")
def test_strict_pull_collects_remote_error_message(self) -> None:
repo = {"verified": {"commit": "expected", "gpg_keys": None}}
with (