Compare commits
No files matched your search
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"ask": [
|
||||||
|
"Skill(update-config)",
|
||||||
|
"Skill(update-config:*)",
|
||||||
|
"Bash(git commit)",
|
||||||
|
"Bash(git commit *)",
|
||||||
|
"Bash(git push)",
|
||||||
|
"Bash(git push *)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sandbox": {
|
||||||
|
"enabled": true,
|
||||||
|
"failIfUnavailable": true,
|
||||||
|
"autoAllowBashIfSandboxed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "00:00"
|
||||||
|
rebase-strategy: "auto"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "python"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "00:00"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "ci"
|
||||||
@@ -2,28 +2,72 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches:
|
||||||
- main
|
- '**'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: global-ci-${{ github.repository }}-${{ github.ref_name }}
|
||||||
|
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:
|
||||||
|
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
|
||||||
@@ -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@v7
|
||||||
|
|
||||||
|
- name: Run hadolint (produce SARIF)
|
||||||
|
id: hadolint
|
||||||
|
continue-on-error: true
|
||||||
|
uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387
|
||||||
|
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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Ruff (Python code sniffer)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-python:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v7
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install ruff
|
||||||
|
run: pip install ruff
|
||||||
|
|
||||||
|
- name: Run ruff
|
||||||
|
run: |
|
||||||
|
ruff check src tests
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: ShellCheck
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-shell:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Install ShellCheck
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y shellcheck
|
||||||
|
- name: Run ShellCheck
|
||||||
|
run: shellcheck -x $(find scripts -type f -name '*.sh' -print)
|
||||||
@@ -1,102 +1,39 @@
|
|||||||
name: Mark stable commit
|
name: Mark stable commit
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mark-stable-${{ github.repository }}-main
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main # still run tests for main
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # run tests for version tags (e.g. v0.9.1)
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
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
|
|
||||||
|
|
||||||
mark-stable:
|
mark-stable:
|
||||||
needs:
|
|
||||||
- test-unit
|
|
||||||
- test-integration
|
|
||||||
- test-env-nix
|
|
||||||
- test-env-virtual
|
|
||||||
- test-e2e
|
|
||||||
- test-virgin-user
|
|
||||||
- test-virgin-root
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 330
|
||||||
# Only run this job if the push is for a version tag (v*)
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # Required to move/update the tag
|
actions: read
|
||||||
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
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}."
|
|
||||||
@@ -4,63 +4,118 @@ on:
|
|||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Mark stable commit"]
|
workflows: ["Mark stable commit"]
|
||||||
types: [completed]
|
types: [completed]
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
prepare:
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
releases: ${{ steps.info.outputs.releases }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository (with tags)
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Resolve the releases this run publishes
|
||||||
|
id: info
|
||||||
|
env:
|
||||||
|
WORKFLOW_RUN_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
run: bash scripts/github/publish-containers/compute-publish-container-info.sh
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: prepare
|
||||||
|
if: ${{ needs.prepare.result == 'success' && needs.prepare.outputs.releases != '[]' }}
|
||||||
|
name: Build ${{ matrix.distro }} ${{ matrix.release.version }} for ${{ matrix.target.platform }}
|
||||||
|
runs-on: ${{ matrix.target.runner }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
distro: &distros [arch, debian, ubuntu, fedora, centos]
|
||||||
|
target:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
release: ${{ fromJSON(needs.prepare.outputs.releases) }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout ${{ matrix.release.version }}
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.release.sha }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003
|
||||||
|
with:
|
||||||
|
use: true
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push ${{ matrix.distro }} ${{ matrix.release.version }} for ${{ matrix.target.platform }}
|
||||||
|
env:
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
VERSION: ${{ matrix.release.version }}
|
||||||
|
PKGMGR_DISTRO: ${{ matrix.distro }}
|
||||||
|
PLATFORM: ${{ matrix.target.platform }}
|
||||||
|
run: bash scripts/github/publish-containers/publish-container-images.sh
|
||||||
|
|
||||||
|
merge:
|
||||||
|
needs: [prepare, build]
|
||||||
|
if: ${{ !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.releases != '[]' }}
|
||||||
|
name: Merge ${{ matrix.distro }} ${{ matrix.release.version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
distro: *distros
|
||||||
|
release: ${{ fromJSON(needs.prepare.outputs.releases) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository (with tags)
|
- name: Checkout ${{ matrix.release.version }}
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
ref: ${{ matrix.release.sha }}
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- 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)"
|
|
||||||
[[ -n "$V_TAG" ]] || { echo "No version tag found"; exit 1; }
|
|
||||||
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 "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "is_stable=${IS_STABLE}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003
|
||||||
with:
|
with:
|
||||||
use: true
|
use: true
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Publish all images
|
- name: Join both platforms of ${{ matrix.distro }} ${{ matrix.release.version }} under its tags
|
||||||
run: |
|
env:
|
||||||
set -euo pipefail
|
OWNER: ${{ github.repository_owner }}
|
||||||
OWNER="${{ github.repository_owner }}" \
|
VERSION: ${{ matrix.release.version }}
|
||||||
VERSION="${{ steps.info.outputs.version }}" \
|
ALIASES: ${{ matrix.release.aliases }}
|
||||||
IS_STABLE="${{ steps.info.outputs.is_stable }}" \
|
PKGMGR_DISTRO: ${{ matrix.distro }}
|
||||||
bash scripts/build/publish.sh
|
run: bash scripts/github/publish-containers/merge-container-images.sh
|
||||||
@@ -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@v7
|
||||||
|
|
||||||
|
- 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,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
|
||||||
@@ -11,10 +14,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
distro: [arch, debian, ubuntu, fedora, centos]
|
distro: [arch, debian, ubuntu, fedora, centos]
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
@@ -22,4 +27,4 @@ jobs:
|
|||||||
- name: Run E2E tests via make (${{ matrix.distro }})
|
- name: Run E2E tests via make (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make test-e2e
|
PKGMGR_DISTRO="${{ matrix.distro }}" make test-e2e
|
||||||
@@ -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
|
||||||
@@ -12,10 +15,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
distro: [arch, debian, ubuntu, fedora, centos]
|
distro: [arch, debian, ubuntu, fedora, centos]
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
@@ -23,4 +28,4 @@ jobs:
|
|||||||
- name: Nix flake-only test (${{ matrix.distro }})
|
- name: Nix flake-only test (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make test-env-nix
|
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-nix
|
||||||
@@ -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
|
||||||
@@ -11,10 +14,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
distro: [arch, debian, ubuntu, fedora, centos]
|
distro: [arch, debian, ubuntu, fedora, centos]
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Show commit SHA
|
- name: Show commit SHA
|
||||||
run: git rev-parse HEAD
|
run: git rev-parse HEAD
|
||||||
@@ -25,4 +30,4 @@ jobs:
|
|||||||
- name: Run container tests (${{ matrix.distro }})
|
- name: Run container tests (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make test-env-virtual
|
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-virtual
|
||||||
@@ -3,17 +3,22 @@ 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
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
- name: Run integration tests via make (Arch container)
|
- name: Run integration tests via make (Arch container)
|
||||||
run: make test-integration distro="arch"
|
run: make test-integration PKGMGR_DISTRO="arch"
|
||||||
@@ -3,17 +3,22 @@ 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
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
- name: Run unit tests via make (Arch container)
|
- name: Run unit tests via make (Arch container)
|
||||||
run: make test-unit distro="arch"
|
run: make test-unit PKGMGR_DISTRO="arch"
|
||||||
@@ -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
|
||||||
@@ -11,46 +14,43 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
distro: [arch, debian, ubuntu, fedora, centos]
|
distro: [arch, debian, ubuntu, fedora, centos]
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
# 🔹 BUILD virgin image if missing
|
- name: Pull base image (${{ matrix.distro }})
|
||||||
- name: Build virgin container (${{ matrix.distro }})
|
run: docker pull "ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest"
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
distro="${{ matrix.distro }}" make build-missing-virgin
|
|
||||||
|
|
||||||
# 🔹 RUN test inside virgin image
|
|
||||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)
|
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$PWD":/src \
|
-v "$PWD":/opt/src/pkgmgr \
|
||||||
-v pkgmgr_repos:/root/Repositories \
|
-v pkgmgr_repos:/root/Repositories \
|
||||||
-v pkgmgr_pip_cache:/root/.cache/pip \
|
-v pkgmgr_pip_cache:/root/.cache/pip \
|
||||||
-w /src \
|
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||||
"pkgmgr-${{ matrix.distro }}-virgin" \
|
-w /opt/src/pkgmgr \
|
||||||
|
"ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
git config --global --add safe.directory /src
|
git config --global --add safe.directory /opt/src/pkgmgr
|
||||||
|
|
||||||
make install
|
make install
|
||||||
make setup
|
make setup
|
||||||
|
|
||||||
. "$HOME/.venvs/pkgmgr/bin/activate"
|
. "$HOME/.venvs/pkgmgr/bin/activate"
|
||||||
|
|
||||||
export NIX_CONFIG="experimental-features = nix-command flakes"
|
|
||||||
|
|
||||||
pkgmgr update pkgmgr --clone-mode shallow --no-verification
|
pkgmgr update pkgmgr --clone-mode shallow --no-verification
|
||||||
pkgmgr version pkgmgr
|
pkgmgr version pkgmgr
|
||||||
|
|
||||||
echo ">>> Running Nix-based: nix run .#pkgmgr -- version pkgmgr"
|
echo ">>> Running Nix-based: nix run .#pkgmgr -- version pkgmgr"
|
||||||
nix run /src#pkgmgr -- version pkgmgr
|
nix run /opt/src/pkgmgr#pkgmgr -- version pkgmgr
|
||||||
'
|
'
|
||||||
@@ -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
|
||||||
@@ -11,29 +14,28 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
distro: [arch, debian, ubuntu, fedora, centos]
|
distro: [arch, debian, ubuntu, fedora, centos]
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
# 🔹 BUILD virgin image if missing
|
- name: Pull base image (${{ matrix.distro }})
|
||||||
- name: Build virgin container (${{ matrix.distro }})
|
run: docker pull "ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest"
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
distro="${{ matrix.distro }}" make build-missing-virgin
|
|
||||||
|
|
||||||
# 🔹 RUN test inside virgin image as non-root
|
|
||||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)
|
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$PWD":/src \
|
-v "$PWD":/opt/src/pkgmgr \
|
||||||
-w /src \
|
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||||
"pkgmgr-${{ matrix.distro }}-virgin" \
|
-w /opt/src/pkgmgr \
|
||||||
|
"ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -42,24 +44,25 @@ jobs:
|
|||||||
useradd -m dev
|
useradd -m dev
|
||||||
echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev
|
echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev
|
||||||
chmod 0440 /etc/sudoers.d/dev
|
chmod 0440 /etc/sudoers.d/dev
|
||||||
chown -R dev:dev /src
|
chown -R dev:dev /opt/src/pkgmgr
|
||||||
|
|
||||||
mkdir -p /nix/store /nix/var/nix /nix/var/log/nix /nix/var/nix/profiles
|
mkdir -p /nix/store /nix/var/nix /nix/var/log/nix /nix/var/nix/profiles
|
||||||
chown -R dev:dev /nix
|
chown -R dev:dev /nix
|
||||||
chmod 0755 /nix
|
chmod 0755 /nix
|
||||||
chmod 1777 /nix/store
|
chmod 1777 /nix/store
|
||||||
|
sudo -H -u dev env \
|
||||||
|
HOME=/home/dev \
|
||||||
|
NIX_CONFIG="$NIX_CONFIG" \
|
||||||
|
PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 \
|
||||||
|
bash -lc "
|
||||||
|
set -euo pipefail
|
||||||
|
cd /opt/src/pkgmgr
|
||||||
|
make setup-venv
|
||||||
|
. \"\$HOME/.venvs/pkgmgr/bin/activate\"
|
||||||
|
|
||||||
sudo -H -u dev env HOME=/home/dev PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 bash -lc "
|
pkgmgr version pkgmgr
|
||||||
set -euo pipefail
|
|
||||||
cd /src
|
|
||||||
|
|
||||||
make setup-venv
|
export NIX_REMOTE=local
|
||||||
. \"\$HOME/.venvs/pkgmgr/bin/activate\"
|
nix run /opt/src/pkgmgr#pkgmgr -- version pkgmgr
|
||||||
|
"
|
||||||
pkgmgr version pkgmgr
|
|
||||||
|
|
||||||
export NIX_REMOTE=local
|
|
||||||
export NIX_CONFIG=\"experimental-features = nix-command flakes\"
|
|
||||||
nix run /src#pkgmgr -- version pkgmgr
|
|
||||||
"
|
|
||||||
'
|
'
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: Update flake.lock
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
name: Update flake.lock
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@v23
|
||||||
|
|
||||||
|
- name: Update flake.lock and open a pull request
|
||||||
|
uses: DeterminateSystems/update-flake-lock@v29
|
||||||
|
with:
|
||||||
|
pr-title: "chore(deps): update flake.lock"
|
||||||
|
pr-labels: |
|
||||||
|
dependencies
|
||||||
|
nix
|
||||||
+1
-2
@@ -24,10 +24,9 @@ package-manager-*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Nix Cache to speed up tests
|
# Nix cache to speed up tests
|
||||||
.nix/
|
.nix/
|
||||||
.nix-dev-installed
|
.nix-dev-installed
|
||||||
flake.lock
|
|
||||||
|
|
||||||
# Ignore logs
|
# Ignore logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
## Scope against the skills repository
|
||||||
|
|
||||||
|
package-manager holds deterministic code that an operator can run on its own,
|
||||||
|
without an agent: repository, branch, worktree, release, packaging and
|
||||||
|
development tooling. Agent coordination (claims, messages, queues, scores,
|
||||||
|
prompts, session data) belongs to the
|
||||||
|
[skills repository](https://github.com/kevinveenbirkenbach/skills), whose skills
|
||||||
|
stay thin wrappers that call `pkgmgr`.
|
||||||
|
|
||||||
|
- A new helper that a human could sensibly run alone goes here as a `pkgmgr`
|
||||||
|
command, with its tests, never as a script inside a skill.
|
||||||
|
- Output that an agent filters further (claims, busy agents, scores) is
|
||||||
|
emitted as `--json`; the filtering stays in the skill.
|
||||||
|
- When the split of a helper is unclear, the operator decides.
|
||||||
|
|
||||||
|
## Working copy
|
||||||
|
|
||||||
|
The installed `pkgmgr` may run straight from the main checkout, so a
|
||||||
|
half-edited tree breaks it for everyone. Edit only in a worktree
|
||||||
|
(`pkgmgr worktree up <branch>`), never in the main checkout.
|
||||||
+564
-28
@@ -1,11 +1,552 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [2.4.0] - 2026-11-07
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Worktree: *worktree up* and *down* give each branch its own slot
|
||||||
|
- Worktree: *worktree flow* keeps pull sources and push targets per worktree
|
||||||
|
- Worktree: *up --pull* and *--push* record the flow; default is the base
|
||||||
|
- Branch: *branch tree* writes a hardened meta-tree of all repositories
|
||||||
|
- Branch: the tree carries each worktree's merge flow
|
||||||
|
- GC: *gc --report* prints a read-only disk inventory
|
||||||
|
- Review: a branch review renders as a local HTML page
|
||||||
|
- Publish: releases rebuild nightly; stable follows the tested build
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
|
||||||
|
- Push: signs only an unsigned HEAD instead of re-signing the whole range
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Push, release, gc: refuse a repository config that would run a program
|
||||||
|
- Git: hooks named in the config no longer run in hardened git calls
|
||||||
|
- Tests: the trusted PATH fixture comes from os.defpath, not the host
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Push: move repo-local sshCommand, credential helpers, hooks to global config
|
||||||
|
|
||||||
|
## [2.3.0] - 2026-11-01
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Branch: *gc --merged-into REF* treats items merged into REF as merged
|
||||||
|
- Branch: *--merged-into* is repeatable, protects REF and works for *purge*
|
||||||
|
- Branch: *--sizes* adds each worktree's disk usage to the plan
|
||||||
|
- Branch: *--min-age HOURS* skips items with a commit or index that recent
|
||||||
|
|
||||||
|
## [2.2.0] - 2026-11-01
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Push: *--no-sign* pushes unsigned commits as they are; leases still apply
|
||||||
|
- Push: *--rewrite* replaces commits a remote already has (amend, squash)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
|
||||||
|
- Push: *pkgmgr push* signs every unpushed unsigned commit before pushing
|
||||||
|
- Push: each pushurl gets its own lease; mirrors no longer fail as stale
|
||||||
|
- Push: a commit that exists only on a mirror stops the push, nothing lost
|
||||||
|
- Push: without a selection it pushes the current checkout or worktree
|
||||||
|
- Release: signs before tagging and pushes branch and tag like *pkgmgr push*
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Container: images build on native amd64 and arm64 runners, not QEMU
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Push: needs a working signing key; pass *--no-sign* to push unsigned
|
||||||
|
- Container: no 2.1.0 or 2.1.1 image reached GHCR; notes below carry over
|
||||||
|
- Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
## [2.1.1] - 2026-10-31
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Release: tool-only or dynamic-version pyproject.toml is skipped, no crash
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Container: no 2.1.0 image reached GHCR; the two notes below carry over
|
||||||
|
- Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
## [2.1.0] - 2026-10-31
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Branch: *pkgmgr branch gc* removes merged clean branches, worktrees, stacks
|
||||||
|
- Branch: *gc* only prints its plan; *--apply* acts, *--json* for scripts
|
||||||
|
- Branch: *pkgmgr branch purge* deletes unmerged or dirty items after y/N
|
||||||
|
- Release: Cargo.toml version and its Cargo.lock entry follow the release
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
|
||||||
|
- Release: starts from the highest version in the tag and all version files
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
## [2.0.5] - 2026-10-29
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Config: *pkgmgr config edit* opens nano instead of aborting without a TTY
|
||||||
|
- Config: *pkgmgr config edit* exits with the editor's code when it fails
|
||||||
|
|
||||||
|
## [2.0.4] - 2026-10-19
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- CI: Python lint passes again on ruff 0.17 and still on 0.16
|
||||||
|
- CI: actions bumped: *actions/checkout* v7, *hadolint-action* 3.5.0
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- 2.0.3 was never marked *stable* nor published to GHCR: its CI failed
|
||||||
|
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||||
|
|
||||||
|
## [2.0.3] - 2026-10-18
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Release: the entry is linted inside the CHANGELOG.md it lands in
|
||||||
|
- Release: a list marker that breaks the file's consistency is caught (MD004)
|
||||||
|
- Release: findings the file already has are not blamed on the entry
|
||||||
|
- Release: *--no-globs* keeps the target repo's other files out of the lint
|
||||||
|
- Release: without markdownlint-cli2, list items must use dash bullets
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||||
|
|
||||||
|
## [2.0.2] - 2026-10-01
|
||||||
|
|
||||||
|
Fixes a release flow that could reject every entry written for it.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- A heading in a changelog message became bold, which markdown-lint reads
|
||||||
|
back as a heading and refuses with MD036
|
||||||
|
- Since the editor reopens on failure, no structured entry could ever pass
|
||||||
|
- Headings now move below the release heading they are filed under
|
||||||
|
- The lint findings reached nobody: the editor took over the terminal in the
|
||||||
|
same breath they were printed
|
||||||
|
- They are held until Enter and repeated inside the editor, above the text
|
||||||
|
- The Makefile installer accepted install-* as proof of an install target but
|
||||||
|
always ran plain make install
|
||||||
|
- A repository with install-python and no install died on "No rule to make
|
||||||
|
target" instead of being skipped
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- transform and lint were covered separately, so a transform producing lint
|
||||||
|
errors went unseen; one test now runs the first through the second
|
||||||
|
- Three tests were checking behaviour the code had already left behind
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- docker/login-action 3.7.0 to 4.6.0, setup-buildx-action 3.12.0 to 4.4.0
|
||||||
|
- actions/setup-python 5 to 7
|
||||||
|
- DeterminateSystems nix-installer-action 16 to 23, update-flake-lock 24 to 29
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
- Nothing to do; a changelog entry written with headings now keeps them
|
||||||
|
|
||||||
|
## [2.0.1] - 2026-09-18
|
||||||
|
|
||||||
|
* A release no longer ends *CHANGELOG.md* on a blank line, which is MD012.
|
||||||
|
* The entry's separator only belongs there when another entry follows it.
|
||||||
|
* Affects the first entry a file receives, so it shows on an opening release.
|
||||||
|
* Four tests cover the placements in *_insert_after_h1*, one per branch.
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-09-18
|
||||||
|
|
||||||
|
Breaking
|
||||||
|
|
||||||
|
* The *virgin* and *slim* image variants are gone; both tag suffixes with them.
|
||||||
|
* Building an image now requires the base images to exist in the registry.
|
||||||
|
* *make build-missing-virgin* is removed; *build* targets build the one image.
|
||||||
|
* The five *BASE_IMAGE_** overrides are gone; *BASE_IMAGES_TAG* replaces them.
|
||||||
|
* *scripts/build/image.sh* loses its *--target* axis.
|
||||||
|
* A repository is installed by one hook, no longer by pip and *make* both.
|
||||||
|
|
||||||
|
Changed
|
||||||
|
|
||||||
|
* *INSTALLERS* is a preference order led by *make install*, first success wins.
|
||||||
|
* A failed hook falls back to the next; all failing exits with what was tried.
|
||||||
|
* The *Dockerfile* builds *FROM ${BASE_IMAGE}*, resolved by *base.sh*.
|
||||||
|
* Publishing pushes *--platform* manifest lists for amd64 and arm64.
|
||||||
|
* *manjaro* can be built; *os_resolver.sh* already maps it onto arch.
|
||||||
|
* The virgin workflows pull the base image instead of building it.
|
||||||
|
* The Nix dev shell moves to Python 3.12 for *portfolio*'s *>=3.12* floor.
|
||||||
|
* It gains *pkg-config*, *openldap* and *cyrus_sasl* for *python-ldap*.
|
||||||
|
* *requires-python* for pkgmgr itself is unchanged at *>=3.9*.
|
||||||
|
* The e2e sweep drops *--silent*, which hid the non-zero exit, not the output.
|
||||||
|
* The README drops the *Virgin* type and the Nix claim the image never met.
|
||||||
|
* It now lists the tags *compute_publish_tags()* pushes, alias included.
|
||||||
|
* Dependabot covers pip and github-actions weekly on Monday.
|
||||||
|
* A new *update-flake-lock* workflow opens a PR for *flake.lock* the same day.
|
||||||
|
* *test_distro_dependency_scripts_install_gpg_tools* moves to base-images.
|
||||||
|
* *scripts/docker/slim.sh* is deleted with the stage that ran it.
|
||||||
|
|
||||||
|
## [1.16.1] - 2026-07-29
|
||||||
|
|
||||||
|
* *pkgmgr repos create* now creates *main*-based repositories: *git init* is run with an explicit *-b main* instead of inheriting the host's *init.defaultBranch*, and a failed initial push no longer renames the branch back to *master*. *resolve_base_branch()* keeps its main → master fallback for existing repositories.
|
||||||
|
* Failures are no longer swallowed: 46 blind *except Exception* handlers were narrowed to the exception types the guarded code can actually raise (*OSError*, *UnicodeDecodeError*, TOML/YAML decode errors, *ValueError*, *ImportError*, *subprocess.SubprocessError*, *GitRunError*, *PackageNotFoundError*). Three handlers stay open by design — the per-repository loops in *install* and *update* are batch boundaries where one failing repository must not abort the run.
|
||||||
|
* Keyring backend errors now surface as the concrete *KeyringOperationError*, raised at the boundary in *KeyringTokenProvider.get/set* with the original exception preserved as *__cause__*, and kept distinct from *KeyringUnavailableError* ("no usable backend" vs. "backend present, call failed"). Both degradation paths are unchanged.
|
||||||
|
* The *code-scanning* report stamps *generated_at* and its fallback output directory in UTC instead of a naive local timestamp.
|
||||||
|
* New *make lint* target runs *ruff* on *src* and *tests* with the same invocation as the *lint-python* workflow, and *make test* now depends on it. Repository-wide formatting and typing modernisation (PEP 604 unions with *from __future__ import annotations*, builtin generics, shebangs stripped from 72 importable modules) — *ruff check src tests* passes.
|
||||||
|
* New *tests/integration/test_error_path_degradation.py* drives 44 of the narrowed handlers into their except branch with real injected faults and asserts the documented degradation.
|
||||||
|
|
||||||
|
## [1.16.0] - 2026-06-28
|
||||||
|
|
||||||
|
* New *code-scanning* command: *pkgmgr code-scanning* downloads a repository's GitHub code scanning alerts and analysis metadata via the *gh* CLI into a timestamped directory (default */tmp/<repo>/code-scanner/<timestamp>*), together with a readable summary, for offline analysis.
|
||||||
|
* The release flow now lints and normalises the changelog message before writing it: a leading heading becomes bold, inline code becomes italic, and the entry is validated against the repository's markdown rules, re-prompting interactively until it is clean.
|
||||||
|
* The Debian changelog and the RPM *%changelog* now mirror the full multi-line message correctly — every change line is indented or dash-prefixed — fixing a package build failure where un-indented bodies broke *dpkg-buildpackage* and *rpmspec*; the changelog entry no longer gains an auto-inserted leading bullet.
|
||||||
|
|
||||||
|
## [1.15.2] - 2026-05-28
|
||||||
|
|
||||||
|
* Restore `infinito` as an alias for the infinito-nexus/core repository so `pkgmgr install infinito` (and friends) resolves again.
|
||||||
|
|
||||||
|
## [1.15.1] - 2026-05-28
|
||||||
|
|
||||||
|
* Insert pkgmgr release changelog entry under the H1 instead of above it. Fixes the markdownlint MD041 (first-line-h1) and MD012 (no-multiple-blanks) regressions that previously trashed every CHANGELOG.md after a release.
|
||||||
|
|
||||||
|
## [1.15.0] - 2026-05-28
|
||||||
|
|
||||||
|
* Add pkgmgr archive subcommand: promote fully-checked NNN-topic.md spec files into the directorys README Archive section and delete the source files. Lookup pattern, README path, and template handling are configurable. Extracted from infinito-nexus-core so every kpmx-managed repo gets the same archival flow.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.14.0] - 2026-05-27
|
||||||
|
|
||||||
|
* Added
|
||||||
|
|
||||||
|
* New release --retry mode re-deploys the HEAD release without
|
||||||
|
re-tagging or modifying any files. It re-pushes the existing version
|
||||||
|
tag, re-aligns the floating latest tag, and (unless --no-publish)
|
||||||
|
re-runs publish. Use this to recover from a release whose post-tag
|
||||||
|
push or PyPI upload failed mid-flight. The release_type argument
|
||||||
|
becomes optional under --retry.
|
||||||
|
* New module pkgmgr.actions.release.retry hosts the retry_release
|
||||||
|
helper so the workflow orchestrator stays focused on the forward
|
||||||
|
path.
|
||||||
|
* RepoPaths now exposes a debian_control slot, discovered alongside
|
||||||
|
debian_changelog under both packaging/debian and the legacy debian
|
||||||
|
layout.
|
||||||
|
* pkgmgr.actions.release.package_name.resolve_package_name centralises
|
||||||
|
the distro-name lookup chain and is unit-tested under
|
||||||
|
tests/unit/pkgmgr/actions/release/test_package_name.py.
|
||||||
|
* tests/unit/pkgmgr/actions/release/test_retry.py covers routing,
|
||||||
|
idempotent push, latest-tag re-alignment, missing-tag error path,
|
||||||
|
and branch-detection fallback.
|
||||||
|
|
||||||
|
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 no longer silently flips the debian/changelog top entry and
|
||||||
|
the RPM changelog stanza to a new identifier. Those keep matching
|
||||||
|
the authoritative value in the packaging files, which is what apt,
|
||||||
|
pacman, and dnf index against.
|
||||||
|
|
||||||
|
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
|
||||||
|
on the next release.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.10.0] - 2026-01-20
|
||||||
|
|
||||||
|
* Introduce safe verbose image cleanup to reduce Docker image size and build artifacts
|
||||||
|
|
||||||
|
## [1.9.5] - 2026-01-16
|
||||||
|
|
||||||
|
* Release patch: improve git pull error diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
## [1.9.4] - 2026-01-13
|
||||||
|
|
||||||
|
* fix(ci): replace sudo with su for user switching to avoid PAM failures in minimal container images
|
||||||
|
|
||||||
|
|
||||||
|
## [1.9.3] - 2026-01-07
|
||||||
|
|
||||||
|
* Made the Nix dependency optional on non-x86_64 architectures to avoid broken Arch Linux ARM repository packages.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.9.2] - 2025-12-21
|
||||||
|
|
||||||
|
* Default configuration files are now packaged and loaded correctly when no user config exists, while fully preserving custom user configurations.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.9.1] - 2025-12-21
|
||||||
|
|
||||||
|
* Fixed installation issues and improved loading of default configuration files.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.9.0] - 2025-12-20
|
||||||
|
|
||||||
|
* * New ***mirror visibility*** command to set remote Git repositories to ***public*** or ***private***.
|
||||||
|
* New ***--public*** flag for ***mirror provision*** to create repositories and immediately make them public.
|
||||||
|
* All configured git mirrors are now provisioned.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.7] - 2025-12-19
|
||||||
|
|
||||||
|
* * **Release version updates now correctly modify ***pyproject.toml*** files that follow PEP 621**, ensuring the ***[project].version*** field is updated as expected.
|
||||||
|
* **Invalid or incomplete ***pyproject.toml*** files are now handled gracefully** with clear error messages instead of abrupt process termination.
|
||||||
|
* **RPM spec files remain compatible during releases**: existing macros such as ***%{?dist}*** are preserved and no longer accidentally modified.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.6] - 2025-12-17
|
||||||
|
|
||||||
|
* Prevent Rate Limits during GitHub Nix Setups
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.5] - 2025-12-17
|
||||||
|
|
||||||
|
* * Clearer Git error handling, especially when a directory is not a Git repository.
|
||||||
|
* More reliable repository verification with improved commit and GPG signature checks.
|
||||||
|
* Better error messages and overall robustness when working with Git-based workflows.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.9.0] - 2025-12-17
|
||||||
|
|
||||||
|
* Automated release.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.4] - 2025-12-17
|
||||||
|
|
||||||
|
* * Made pkgmgr’s base-layer role explicit by standardizing the Docker/CI mount path to *`/opt/src/pkgmgr`*.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.3] - 2025-12-16
|
||||||
|
|
||||||
|
* MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.2] - 2025-12-16
|
||||||
|
|
||||||
|
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.1] - 2025-12-16
|
||||||
|
|
||||||
|
* * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.
|
||||||
|
* Mirrors are now handled cleanly: only valid Git remotes are used for Git operations, while non-Git URLs (e.g. PyPI) are excluded, preventing broken or confusing repository configs.
|
||||||
|
* GitHub authentication is more robust: tokens are automatically resolved via the GitHub CLI (`gh`), invalid stored tokens are replaced, and interactive prompts occur only when necessary.
|
||||||
|
* Repository creation and release workflows are more reliable, producing cleaner Git configurations and more predictable version handling.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.0] - 2025-12-15
|
||||||
|
|
||||||
|
* *** New Features: ***
|
||||||
|
- **Silent Updates**: You can now use the `--silent` flag during installs and updates to suppress error messages for individual repositories and get a single summary at the end. This ensures the process continues even if some repositories fail, while still preserving interactive checks when not in silent mode.
|
||||||
|
- **Repository Scaffolding**: The process for creating new repositories has been improved. You can now use templates to scaffold repositories with a preview and automatic mirror setup.
|
||||||
|
|
||||||
|
*** Bug Fixes: ***
|
||||||
|
- **Pip Installation**: Pip is now installed automatically on all supported systems. This includes `python-pip` for Arch and `python3-pip` for CentOS, Debian, Fedora, and Ubuntu, ensuring that pip is available for Python package installations.
|
||||||
|
- **Pacman Keyring**: Fixed an issue on Arch Linux where package installation would fail due to missing keys. The pacman keyring is now properly initialized before installing packages.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.7.2] - 2025-12-15
|
||||||
|
|
||||||
|
* * Git mirrors are now resolved consistently (origin → MIRRORS file → config → default).
|
||||||
|
* The `origin` remote is always enforced to use the primary URL for both fetch and push.
|
||||||
|
* Additional mirrors are added as extra push targets without duplication.
|
||||||
|
* Local and remote mirror setup behaves more predictably and consistently.
|
||||||
|
* Improved test coverage ensures stable origin and push URL handling.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.7.1] - 2025-12-14
|
||||||
|
|
||||||
|
* Patched package-manager to kpmx to publish on pypi
|
||||||
|
|
||||||
|
|
||||||
|
## [1.7.0] - 2025-12-14
|
||||||
|
|
||||||
|
* * New *pkgmgr publish* command to publish repository artifacts to PyPI based on the *MIRRORS* file.
|
||||||
|
* Automatically selects the current repository when no explicit selection is given.
|
||||||
|
* Publishes only when a semantic version tag is present on *HEAD*; otherwise skips with a clear info message.
|
||||||
|
* Supports non-interactive mode for CI environments via *--non-interactive*.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.6.4] - 2025-12-14
|
||||||
|
|
||||||
|
* * Improved reliability of Nix installs and updates, including automatic resolution of profile conflicts and better handling of GitHub 403 rate limits.
|
||||||
|
* More stable launcher behavior in packaged and virtual-env setups.
|
||||||
|
* Enhanced mirror and remote handling: repository owner/name are derived from URLs, with smoother provisioning and clearer credential handling.
|
||||||
|
* More reliable releases and artifacts due to safer CI behavior when no version tag is present.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.6.3] - 2025-12-14
|
||||||
|
|
||||||
|
* ***Fixed:*** Corrected repository path resolution so release and version logic consistently use the canonical packaging/* layout, preventing changelog and packaging files from being read or updated from incorrect locations.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.6.2] - 2025-12-14
|
||||||
|
|
||||||
|
* **pkgmgr version** now also shows the installed pkgmgr version when run outside a repository.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.6.1] - 2025-12-14
|
||||||
|
|
||||||
|
* * Added automatic retry handling for GitHub 403 / rate-limit errors during Nix flake installs (Fibonacci backoff with jitter).
|
||||||
|
|
||||||
|
|
||||||
|
## [1.6.0] - 2025-12-14
|
||||||
|
|
||||||
|
* *** Changed ***
|
||||||
|
- Unified update handling via a single top-level `pkgmgr update` command, removing ambiguous update paths.
|
||||||
|
- Improved update reliability by routing all update logic through a central UpdateManager.
|
||||||
|
- Renamed system update flag from `--system-update` to `--system` for clarity and consistency.
|
||||||
|
- Made mirror handling explicit and safer by separating setup, check, and provision responsibilities.
|
||||||
|
- Improved credential resolution for remote providers (environment → keyring → interactive).
|
||||||
|
|
||||||
|
*** Added ***
|
||||||
|
- Optional system updates via `pkgmgr update --system` (Arch, Debian/Ubuntu, Fedora/RHEL).
|
||||||
|
- `pkgmgr install --update` to force re-running installers and refresh existing installations.
|
||||||
|
- Remote repository provisioning for mirrors on supported providers.
|
||||||
|
- Extended end-to-end test coverage for update and mirror workflows.
|
||||||
|
|
||||||
|
*** Fixed ***
|
||||||
|
- Resolved “Unknown repos command: update” errors after CLI refactoring.
|
||||||
|
- Improved Nix update stability and reduced CI failures caused by transient rate limits.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.0] - 2025-12-13
|
||||||
|
|
||||||
|
* - Commands now show live output while running, making long operations easier to follow
|
||||||
|
- Error messages include full command output, making failures easier to understand and debug
|
||||||
|
- Deinstallation is more complete and predictable, removing CLI links and properly cleaning up repositories
|
||||||
|
- Preview mode is more trustworthy, clearly showing what would happen without making changes
|
||||||
|
- Repository configuration problems are detected earlier with clear, user-friendly explanations
|
||||||
|
- More consistent behavior across different Linux distributions
|
||||||
|
- More reliable execution in Docker containers and CI environments
|
||||||
|
- Nix-based execution works more smoothly, especially when running as root or inside containers
|
||||||
|
- Existing commands, scripts, and workflows continue to work without any breaking changes
|
||||||
|
|
||||||
|
|
||||||
## [1.4.1] - 2025-12-12
|
## [1.4.1] - 2025-12-12
|
||||||
|
|
||||||
* Fixed (#1) stable release container publishing
|
* Fixed stable release container publishing
|
||||||
|
|
||||||
|
|
||||||
## [1.4.0] - 2025-12-12
|
## [1.4.0] - 2025-12-12
|
||||||
|
|
||||||
* **Docker Container Building**
|
**Docker Container Building**
|
||||||
|
|
||||||
* New official container images are automatically published on each release.
|
* New official container images are automatically published on each release.
|
||||||
* Images are available per distribution and as a default Arch-based image.
|
* Images are available per distribution and as a default Arch-based image.
|
||||||
@@ -19,7 +560,7 @@
|
|||||||
|
|
||||||
## [1.3.0] - 2025-12-12
|
## [1.3.0] - 2025-12-12
|
||||||
|
|
||||||
* **Minor release – Stability & CI hardening**
|
**Stability & CI hardening**
|
||||||
|
|
||||||
* Stabilized Nix resolution and global symlink handling across Arch, CentOS, Debian, and Ubuntu
|
* Stabilized Nix resolution and global symlink handling across Arch, CentOS, Debian, and Ubuntu
|
||||||
* Ensured Nix works reliably in CI, sudo, login, and non-login shells without overriding distro-managed paths
|
* Ensured Nix works reliably in CI, sudo, login, and non-login shells without overriding distro-managed paths
|
||||||
@@ -31,7 +572,7 @@
|
|||||||
|
|
||||||
## [1.2.1] - 2025-12-12
|
## [1.2.1] - 2025-12-12
|
||||||
|
|
||||||
* **Changed**
|
**Changed**
|
||||||
|
|
||||||
* Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities.
|
* Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities.
|
||||||
|
|
||||||
@@ -48,7 +589,7 @@
|
|||||||
|
|
||||||
## [1.2.0] - 2025-12-12
|
## [1.2.0] - 2025-12-12
|
||||||
|
|
||||||
* **Release workflow overhaul**
|
**Release workflow overhaul**
|
||||||
|
|
||||||
* Introduced a fully structured release workflow with clear phases and safeguards
|
* Introduced a fully structured release workflow with clear phases and safeguards
|
||||||
* Added preview-first releases with explicit confirmation before execution
|
* Added preview-first releases with explicit confirmation before execution
|
||||||
@@ -65,7 +606,8 @@
|
|||||||
|
|
||||||
## [1.0.0] - 2025-12-11
|
## [1.0.0] - 2025-12-11
|
||||||
|
|
||||||
* **1.0.0 – Official Stable Release 🎉**
|
**Official Stable Release 🎉**
|
||||||
|
|
||||||
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
|
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -158,7 +700,7 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
|||||||
|
|
||||||
## [0.9.1] - 2025-12-10
|
## [0.9.1] - 2025-12-10
|
||||||
|
|
||||||
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
* Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
||||||
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
|
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
|
||||||
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
|
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
|
||||||
* Removed deprecated files and updated `.gitignore`.
|
* Removed deprecated files and updated `.gitignore`.
|
||||||
@@ -253,47 +795,45 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
|||||||
|
|
||||||
## [0.7.1] - 2025-12-09
|
## [0.7.1] - 2025-12-09
|
||||||
|
|
||||||
* Fix floating 'latest' tag logic: dereference annotated target (vX.Y.Z^{}), add tag message to avoid Git errors, ensure best-effort update without blocking releases, and update unit tests (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff).
|
* Fix floating 'latest' tag logic
|
||||||
|
* dereference annotated target (vX.Y.Z^{})
|
||||||
|
* add tag message to avoid Git errors
|
||||||
|
* ensure best-effort update without blocking releases
|
||||||
|
|
||||||
## [0.7.0] - 2025-12-09
|
## [0.7.0] - 2025-12-09
|
||||||
|
|
||||||
* Add Git helpers for branch sync and floating 'latest' tag in the release workflow, ensure main/master are updated from origin before tagging, and extend unit/e2e tests including 'pkgmgr release --help' coverage (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff)
|
* Add Git helpers for branch sync and floating 'latest' tag in the release workflow
|
||||||
|
* ensure main/master are updated from origin before tagging
|
||||||
|
|
||||||
## [0.6.0] - 2025-12-09
|
## [0.6.0] - 2025-12-09
|
||||||
|
|
||||||
* Expose DISTROS and BASE_IMAGE_* variables as exported Makefile environment variables so all build and test commands can consume them dynamically. By exporting these values, every Make target (e.g., build, build-no-cache, build-missing, test-container, test-unit, test-e2e) and every delegated script in scripts/build/ and scripts/test/ now receives a consistent view of the supported distributions and their base container images. This change removes duplicated definitions across scripts, ensures reproducible builds, and allows build tooling to react automatically when new distros or base images are added to the Makefile.
|
* Consistent view of the supported distributions and their base container images.
|
||||||
|
|
||||||
|
|
||||||
## [0.5.1] - 2025-12-09
|
## [0.5.1] - 2025-12-09
|
||||||
|
|
||||||
* Refine pkgmgr release CLI close wiring and integration tests for --close flag (ChatGPT: https://chatgpt.com/share/69376b4e-8440-800f-9d06-535ec1d7a40e)
|
* Refine pkgmgr release CLI close wiring and integration tests for --close flag
|
||||||
|
|
||||||
|
|
||||||
## [0.5.0] - 2025-12-09
|
## [0.5.0] - 2025-12-09
|
||||||
|
|
||||||
* Add pkgmgr branch close subcommand, extend CLI parser wiring, and add unit tests for branch handling and version version-selection logic (see ChatGPT conversation: https://chatgpt.com/share/693762a3-9ea8-800f-a640-bc78170953d1)
|
* Add pkgmgr branch close subcommand, extend CLI parser wiring
|
||||||
|
|
||||||
|
|
||||||
## [0.4.3] - 2025-12-09
|
## [0.4.3] - 2025-12-09
|
||||||
|
|
||||||
* Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject), and add comprehensive unit/e2e tests for release and branch commands (see ChatGPT conversation: https://chatgpt.com/share/69375cfe-9e00-800f-bd65-1bd5937e1696)
|
* Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject)
|
||||||
|
|
||||||
|
|
||||||
## [0.4.2] - 2025-12-09
|
## [0.4.2] - 2025-12-09
|
||||||
|
|
||||||
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
* Wire pkgmgr release CLI to new helpe
|
||||||
|
|
||||||
|
|
||||||
## [0.4.1] - 2025-12-08
|
## [0.4.1] - 2025-12-08
|
||||||
|
|
||||||
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
* Add branch close subcommand and integrate release close/editor flow
|
||||||
|
|
||||||
|
|
||||||
## [0.4.0] - 2025-12-08
|
## [0.4.0] - 2025-12-08
|
||||||
|
|
||||||
* Add branch closing helper and --close flag to release command, including CLI wiring and tests (see https://chatgpt.com/share/69374aec-74ec-800f-bde3-5d91dfdb9b91)
|
* Add branch closing helper and --close flag to release command
|
||||||
|
|
||||||
## [0.3.0] - 2025-12-08
|
## [0.3.0] - 2025-12-08
|
||||||
|
|
||||||
@@ -304,13 +844,10 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
|||||||
- New config update logic + default YAML sync
|
- New config update logic + default YAML sync
|
||||||
- Improved proxy command handling
|
- Improved proxy command handling
|
||||||
- Full CLI routing refactor
|
- Full CLI routing refactor
|
||||||
- Expanded E2E tests for list, proxy, and selection logic
|
|
||||||
Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
|
|
||||||
|
|
||||||
## [0.2.0] - 2025-12-08
|
## [0.2.0] - 2025-12-08
|
||||||
|
|
||||||
* Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
|
* Add preview-first release workflow and extended packaging support
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0] - 2025-12-08
|
## [0.1.0] - 2025-12-08
|
||||||
|
|
||||||
@@ -319,5 +856,4 @@ Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
|
|||||||
|
|
||||||
## [0.1.0] - 2025-12-08
|
## [0.1.0] - 2025-12-08
|
||||||
|
|
||||||
* Implement unified release helper with preview mode, multi-packaging version bumps, and new integration/unit tests (see ChatGPT conversation 2025-12-08: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
|
* Implement unified release helper with preview mode, multi-packaging version bumps
|
||||||
|
|
||||||
+8
-41
@@ -1,58 +1,25 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# hadolint ignore=DL3006,InvalidDefaultArgInFrom
|
||||||
# Base image selector — overridden by build args / Makefile
|
# BASE_IMAGE carries no default on purpose: a default would build one
|
||||||
# ------------------------------------------------------------
|
# distribution's image under another distribution's tag whenever the build arg
|
||||||
|
# is forgotten. scripts/build/base.sh is the only place that resolves it.
|
||||||
ARG BASE_IMAGE
|
ARG BASE_IMAGE
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Target: virgin
|
|
||||||
# - installs distro deps (incl. make)
|
|
||||||
# - no pkgmgr build
|
|
||||||
# - no entrypoint
|
|
||||||
# ============================================================
|
|
||||||
FROM ${BASE_IMAGE} AS virgin
|
|
||||||
SHELL ["/bin/bash", "-lc"]
|
SHELL ["/bin/bash", "-lc"]
|
||||||
|
|
||||||
RUN echo "BASE_IMAGE=${BASE_IMAGE}" && cat /etc/os-release || true
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# Copy scripts first so dependency installation can be cached
|
|
||||||
COPY scripts/installation/ scripts/installation/
|
|
||||||
|
|
||||||
# Install distro-specific build dependencies (including make)
|
|
||||||
RUN bash scripts/installation/dependencies.sh
|
|
||||||
|
|
||||||
# Virgin default
|
|
||||||
CMD ["bash"]
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Target: full
|
|
||||||
# - inherits from virgin
|
|
||||||
# - builds + installs pkgmgr
|
|
||||||
# - sets entrypoint + default cmd
|
|
||||||
# ============================================================
|
|
||||||
FROM virgin AS full
|
|
||||||
|
|
||||||
# Nix environment defaults (only config; nix itself comes from deps/install flow)
|
|
||||||
ENV NIX_CONFIG="experimental-features = nix-command flakes"
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
# Copy full repository for build
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build and install distro-native package-manager package
|
RUN set -eu; \
|
||||||
RUN set -euo pipefail; \
|
|
||||||
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
|
|
||||||
COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh
|
COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /opt/src/pkgmgr
|
||||||
ENTRYPOINT ["/usr/local/bin/docker-entry.sh"]
|
ENTRYPOINT ["/usr/local/bin/docker-entry.sh"]
|
||||||
CMD ["pkgmgr", "--help"]
|
CMD ["pkgmgr", "--help"]
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
git@github.com:kevinveenbirkenbach/package-manager.git
|
git@github.com:kevinveenbirkenbach/package-manager.git
|
||||||
ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git
|
ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git
|
||||||
ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git
|
ssh://git@code.infinito.nexus:2201/kevinveenbirkenbach/pkgmgr.git
|
||||||
|
https://pypi.org/project/kpmx/
|
||||||
@@ -2,30 +2,24 @@
|
|||||||
build build-no-cache build-no-cache-all build-missing \
|
build build-no-cache build-no-cache-all build-missing \
|
||||||
delete-volumes purge \
|
delete-volumes purge \
|
||||||
test test-unit test-e2e test-integration test-env-virtual test-env-nix \
|
test test-unit test-e2e test-integration test-env-virtual test-env-nix \
|
||||||
|
lint \
|
||||||
setup setup-venv setup-nix
|
setup setup-venv setup-nix
|
||||||
|
|
||||||
# Distro
|
# Distro
|
||||||
# Options: arch debian ubuntu fedora centos
|
# Options: arch debian ubuntu fedora centos
|
||||||
DISTROS ?= arch debian ubuntu fedora centos
|
DISTROS ?= arch debian ubuntu fedora centos
|
||||||
distro ?= arch
|
PKGMGR_DISTRO ?= arch
|
||||||
export distro
|
export PKGMGR_DISTRO
|
||||||
|
|
||||||
|
# Nix Config Variable (To avoid rate limit)
|
||||||
|
NIX_CONFIG ?=
|
||||||
|
export NIX_CONFIG
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Base images
|
# Base images
|
||||||
# (kept for documentation/reference; actual build logic is in scripts/build)
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
BASE_IMAGE_ARCH := archlinux:latest
|
BASE_IMAGES_TAG ?= latest
|
||||||
BASE_IMAGE_DEBIAN := debian:stable-slim
|
export BASE_IMAGES_TAG
|
||||||
BASE_IMAGE_UBUNTU := ubuntu:latest
|
|
||||||
BASE_IMAGE_FEDORA := fedora:latest
|
|
||||||
BASE_IMAGE_CENTOS := quay.io/centos/centos:stream9
|
|
||||||
|
|
||||||
# Make them available in scripts
|
|
||||||
export BASE_IMAGE_ARCH
|
|
||||||
export BASE_IMAGE_DEBIAN
|
|
||||||
export BASE_IMAGE_UBUNTU
|
|
||||||
export BASE_IMAGE_FEDORA
|
|
||||||
export BASE_IMAGE_CENTOS
|
|
||||||
|
|
||||||
# PYthon Unittest Pattern
|
# PYthon Unittest Pattern
|
||||||
TEST_PATTERN := test_*.py
|
TEST_PATTERN := test_*.py
|
||||||
@@ -44,7 +38,7 @@ install:
|
|||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
# Default: keep current auto-detection behavior
|
# Default: keep current auto-detection behavior
|
||||||
setup: setup-nix setup-venv
|
setup: setup-venv
|
||||||
|
|
||||||
# Explicit: developer setup (Python venv + shell RC + install)
|
# Explicit: developer setup (Python venv + shell RC + install)
|
||||||
setup-venv: setup-nix
|
setup-venv: setup-nix
|
||||||
@@ -58,26 +52,28 @@ setup-nix:
|
|||||||
# Docker build targets (delegated to scripts/build)
|
# Docker build targets (delegated to scripts/build)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
build:
|
build:
|
||||||
@bash scripts/build/image.sh --target virgin
|
|
||||||
@bash scripts/build/image.sh
|
@bash scripts/build/image.sh
|
||||||
|
|
||||||
build-missing-virgin:
|
build-missing:
|
||||||
@bash scripts/build/image.sh --target virgin --missing
|
|
||||||
|
|
||||||
build-missing: build-missing-virgin
|
|
||||||
@bash scripts/build/image.sh --missing
|
@bash scripts/build/image.sh --missing
|
||||||
|
|
||||||
build-no-cache:
|
build-no-cache:
|
||||||
@bash scripts/build/image.sh --target virgin --no-cache
|
|
||||||
@bash scripts/build/image.sh --no-cache
|
@bash scripts/build/image.sh --no-cache
|
||||||
|
|
||||||
build-no-cache-all:
|
build-no-cache-all:
|
||||||
@set -e; \
|
@set -e; \
|
||||||
for d in $(DISTROS); do \
|
for d in $(DISTROS); do \
|
||||||
echo "=== build-no-cache: $$d ==="; \
|
echo "=== build-no-cache: $$d ==="; \
|
||||||
distro="$$d" $(MAKE) build-no-cache; \
|
PKGMGR_DISTRO="$$d" $(MAKE) build-no-cache; \
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Lint targets (run on the host, not in a container)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@bash scripts/lint/python.sh
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Test targets (delegated to scripts/test)
|
# Test targets (delegated to scripts/test)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -97,11 +93,11 @@ test-env-virtual: build-missing
|
|||||||
test-env-nix: build-missing
|
test-env-nix: build-missing
|
||||||
@bash scripts/test/test-env-nix.sh
|
@bash scripts/test/test-env-nix.sh
|
||||||
|
|
||||||
# Combined test target for local + CI (unit + integration + e2e)
|
# Combined test target for local + CI (lint + unit + integration + e2e)
|
||||||
test: test-env-virtual test-unit test-integration test-e2e
|
test: lint test-env-virtual test-unit test-integration test-e2e
|
||||||
|
|
||||||
delete-volumes:
|
delete-volumes:
|
||||||
@docker volume rm pkgmgr_nix_store_${distro} pkgmgr_nix_cache_${distro} || true
|
@docker volume rm "pkgmgr_nix_store_${PKGMGR_DISTRO}" "pkgmgr_nix_cache_${PKGMGR_DISTRO}" || echo "No volumes to delete."
|
||||||
|
|
||||||
purge: delete-volumes build-no-cache
|
purge: delete-volumes build-no-cache
|
||||||
|
|
||||||
|
|||||||
@@ -25,52 +25,37 @@ together into repeatable development workflows.
|
|||||||
|
|
||||||
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
|
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
|
||||||
single operating system. PKGMGR instead focuses on **your repositories and
|
single operating system. PKGMGR instead focuses on **your repositories and
|
||||||
development lifecycle**:
|
development lifecycle**. It provides one configuration for all repositories,
|
||||||
|
one unified CLI to interact with them, and a Nix-based foundation that keeps
|
||||||
|
tooling reproducible across distributions.
|
||||||
|
|
||||||
* one configuration for all your repos,
|
Native package managers are still used where they make sense. PKGMGR coordinates
|
||||||
* one CLI to interact with them,
|
the surrounding development, build and release workflows in a consistent way.
|
||||||
* one Nix-based layer to keep tooling reproducible across distros.
|
|
||||||
|
|
||||||
You keep using your native package manager where it makes sense – PKGMGR
|
In addition, PKGMGR provides Docker images that can serve as a **reproducible
|
||||||
coordinates the *development and release flow* around it.
|
system baseline**. These images bundle the complete PKGMGR toolchain and are
|
||||||
|
designed to be reused as a stable execution environment across machines,
|
||||||
|
pipelines and teams. This approach is specifically used within
|
||||||
|
[**Infinito.Nexus**](https://s.infinito.nexus/code) to make complex systems
|
||||||
|
distribution-independent while remaining fully reproducible.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features 🚀
|
## Features 🚀
|
||||||
|
|
||||||
### Multi-distro development & packaging
|
PKGMGR enables multi-distro development and packaging by managing multiple
|
||||||
|
repositories from a single configuration file. It drives complete release
|
||||||
|
pipelines across Linux distributions using Nix flakes, Python build metadata,
|
||||||
|
native OS packages such as Arch, Debian and RPM formats, and additional ecosystem
|
||||||
|
integrations like Ansible.
|
||||||
|
|
||||||
* Manage **many repositories at once** from a single `config/config.yaml`.
|
All functionality is exposed through a unified `pkgmgr` command-line interface
|
||||||
* Drive full **release pipelines** across Linux distributions using:
|
that works identically on every supported distribution. It combines repository
|
||||||
|
management, Git operations, Docker and Compose orchestration, as well as
|
||||||
|
versioning, release and changelog workflows. Many commands support a preview
|
||||||
|
mode, allowing you to inspect the underlying actions before they are executed.
|
||||||
|
|
||||||
* Nix flakes (`flake.nix`)
|
---
|
||||||
* PyPI style builds (`pyproject.toml`)
|
|
||||||
* OS packages (PKGBUILD, Debian control/changelog, RPM spec)
|
|
||||||
* Ansible Galaxy metadata and more.
|
|
||||||
|
|
||||||
### Rich CLI for daily work
|
|
||||||
|
|
||||||
All commands are exposed via the `pkgmgr` CLI and are available on every distro:
|
|
||||||
|
|
||||||
* **Repository management**
|
|
||||||
|
|
||||||
* `clone`, `update`, `install`, `delete`, `deinstall`, `path`, `list`, `config`
|
|
||||||
* **Git proxies**
|
|
||||||
|
|
||||||
* `pull`, `push`, `status`, `diff`, `add`, `show`, `checkout`,
|
|
||||||
`reset`, `revert`, `rebase`, `commit`, `branch`
|
|
||||||
* **Docker & Compose orchestration**
|
|
||||||
|
|
||||||
* `build`, `up`, `down`, `exec`, `ps`, `start`, `stop`, `restart`
|
|
||||||
* **Release toolchain**
|
|
||||||
|
|
||||||
* `version`, `release`, `changelog`, `make`
|
|
||||||
* **Mirror & workflow helpers**
|
|
||||||
|
|
||||||
* `mirror` (list/diff/merge/setup), `shell`, `terminal`, `code`, `explore`
|
|
||||||
|
|
||||||
Many of these commands support `--preview` mode so you can inspect the
|
|
||||||
underlying Git or Docker calls without executing them.
|
|
||||||
|
|
||||||
### Full development workflows
|
### Full development workflows
|
||||||
|
|
||||||
@@ -83,9 +68,88 @@ versioning features it can drive **end-to-end workflows**:
|
|||||||
4. Build distro-specific packages.
|
4. Build distro-specific packages.
|
||||||
5. Keep all mirrors and working copies in sync.
|
5. Keep all mirrors and working copies in sync.
|
||||||
|
|
||||||
The extensive E2E tests (`tests/e2e/`) and GitHub Actions workflows (including
|
---
|
||||||
“virgin user” and “virgin root” Arch tests) validate these flows across
|
|
||||||
different Linux environments.
|
### Worktrees and garbage collection
|
||||||
|
|
||||||
|
| Command | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| `pkgmgr worktree up <branch> [--base REF] [--path DIR] [--pull REF ...] [--push BRANCH ...]` | Checks the branch out under `<directories.worktrees>/<host>/<owner>/<repo>/<branch>` or at `--path`, assigns a free slot, records the merge flow and runs the pinned `worktree-init` hook |
|
||||||
|
| `pkgmgr worktree flow [PATH] [--json]` | Shows the merge flow of the worktree at `PATH` (default: `.`) |
|
||||||
|
| `pkgmgr worktree flow set [PATH] (--pull REF ... \| --no-pull) (--push BRANCH ... \| --no-push) [--json]` | Replaces both lists of the merge flow |
|
||||||
|
| `pkgmgr worktree flow clear [PATH] [--json]` | Removes the merge flow |
|
||||||
|
| `pkgmgr worktree cleanup [--apply]` | Removes clean worktrees whose commits are merged or pushed: pinned `worktree-fini` hook, compose stack, checkout; the branch stays |
|
||||||
|
| `pkgmgr branch cleanup [--apply]` | Removes merged branches that no worktree holds, locally and on the remotes |
|
||||||
|
| `pkgmgr branch tree [--format json\|yaml] [--output FILE]` | Writes every managed repository's branches and worktrees with their cleanup state, without fetching |
|
||||||
|
| `pkgmgr gc [--all] [--apply]` | Both cleanups in one pass, for the current repository or every managed one |
|
||||||
|
| `pkgmgr gc --report [--all] [--root DIR] [--threshold-mb MB] [--docker] [--json]` | Read-only disk inventory: worktree sizes, changes a cleanup would keep, `--root` children, core dumps, docker usage, free space per filesystem |
|
||||||
|
| `pkgmgr worktree orphans --stale-days N --top N` | Lists merged and abandoned branches and worktrees with a proposal each |
|
||||||
|
|
||||||
|
A hook runs only when the `worktree_hooks` config pins its command under the
|
||||||
|
`<provider>/<account>/<repository>` of a repository managed in the pkgmgr
|
||||||
|
config, and only for that repository's directory; origin's URL, which the
|
||||||
|
repository's own config sets, never selects a pin. `{path}` and `{slot}` are
|
||||||
|
replaced; a
|
||||||
|
`worktree-init` or `worktree-fini` target in the repository's Makefile is
|
||||||
|
reported, never run, because an agent may have written it. The pinned program
|
||||||
|
must be an absolute path or a root-owned command on a `PATH` entry that root
|
||||||
|
owns and no group or other user can write, up to `/`, so a binary planted in a
|
||||||
|
user-writable directory such as a virtualenv never runs. The Nix store never
|
||||||
|
counts: it is group-writable, and any user can have the daemon build
|
||||||
|
root-owned content there, so pin an absolute path on Nix. A bare name runs as
|
||||||
|
the resolved file that was checked. Inside a user namespace other than the
|
||||||
|
initial one, root means the overflow uid, under which the real root's files
|
||||||
|
show there, as do those of every other unmapped owner. Outside such a
|
||||||
|
namespace a directory on a nosuid mount, where udisks2 and FUSE put user
|
||||||
|
mounts, never counts as root's. The hook runs with
|
||||||
|
stdin closed, only those `PATH` entries and a 30 minute timeout. Removal walks to
|
||||||
|
the worktree without following a symbolic link and skips a worktree path that
|
||||||
|
is not its own realpath. A pinned command that reads repository files, such as `make` or a
|
||||||
|
script in the repository, trusts the worktree's content, tracked or ignored.
|
||||||
|
|
||||||
|
Every cleanup is a dry run until `--apply`, and `--json` prints the plan for
|
||||||
|
other tools. The [agent skills](https://github.com/kevinveenbirkenbach/skills)
|
||||||
|
wrap these commands; see [AGENTS.md](AGENTS.md) for the split.
|
||||||
|
|
||||||
|
#### Merge flow
|
||||||
|
|
||||||
|
Every worktree can carry a merge flow that scripts and skills read: `pull`
|
||||||
|
lists the refs merged into the branch, in order, before a gate, deploy or
|
||||||
|
merge; `push` lists the branches the branch is merged into, in order, when it
|
||||||
|
is finished, each then pushed by the operator. It lives in
|
||||||
|
`pkgmgr/flow.yml` inside the worktree's own git directory (`.git` of the main
|
||||||
|
worktree, `<common>/worktrees/<id>` of a linked one), so it is never committed
|
||||||
|
and goes with the worktree:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 1
|
||||||
|
branch: feature/x
|
||||||
|
pull:
|
||||||
|
- origin/main
|
||||||
|
- store-i18n
|
||||||
|
push:
|
||||||
|
- feature/iam-profile-rbac
|
||||||
|
- production
|
||||||
|
```
|
||||||
|
|
||||||
|
`branch` is the branch the flow was set on; another current branch shows as a
|
||||||
|
warning. `worktree up` records `--pull` and `--push`; without them pull is
|
||||||
|
the base the worktree came from and push that base's local branch
|
||||||
|
(`origin/main` gives `main`). `flow set` needs each list or
|
||||||
|
its `--no-pull`/`--no-push`, and writes atomically into a `pkgmgr/` of mode
|
||||||
|
0700. Every entry must pass `git check-ref-format --branch` and must not start
|
||||||
|
with `-`; a remote-tracking name such as `origin/main` passes. An invalid name
|
||||||
|
exits 2 and writes nothing. A flow file that is a symbolic link, no regular
|
||||||
|
file, larger than 64 KiB or malformed reads as `invalid`, with empty lists and
|
||||||
|
a warning, and is never followed.
|
||||||
|
|
||||||
|
`flow --json` prints `worktree` and `git_dir` (absolute paths), `branch` (the
|
||||||
|
current branch, `null` when detached), `flow_branch` (the file's `branch`),
|
||||||
|
`pull`, `push`, `source` (`file`, `none` or `invalid`) and `warnings`; it
|
||||||
|
exits 0 also without a flow, 2 when `PATH` is not a git worktree. In
|
||||||
|
`pkgmgr branch tree` every item carries `flow`: `{"pull", "push", "source"}`,
|
||||||
|
or `null` without a worktree or a flow; flow warnings join the repository's
|
||||||
|
`warnings`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -99,25 +163,26 @@ The following diagram gives a full overview of:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
**Diagram status:** 12 December 2025
|
**Diagram status:** 12 December 2025
|
||||||
|
|
||||||
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
|
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Perfekt, dann hier die **noch kompaktere und korrekt differenzierte Version**, die **nur** zwischen
|
|
||||||
**`make setup`** und **`make setup-venv`** unterscheidet und exakt deinem Verhalten entspricht.
|
|
||||||
|
|
||||||
README-ready, ohne Over-Engineering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation ⚙️
|
## Installation ⚙️
|
||||||
|
|
||||||
PKGMGR can be installed using `make`.
|
PKGMGR can be installed using `make`.
|
||||||
The setup mode defines **which runtime layers are prepared**.
|
The setup mode defines **which runtime layers are prepared**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||||
|
cd package-manager
|
||||||
|
```
|
||||||
|
|
||||||
### Dependency installation (optional)
|
### Dependency installation (optional)
|
||||||
|
|
||||||
System dependencies required **before running any *make* commands** are installed via:
|
System dependencies required **before running any *make* commands** are installed via:
|
||||||
@@ -128,8 +193,13 @@ scripts/installation/dependencies.sh
|
|||||||
|
|
||||||
The script detects and normalizes the OS and installs the required **system-level dependencies** accordingly.
|
The script detects and normalizes the OS and installs the required **system-level dependencies** accordingly.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
---
|
```bash
|
||||||
|
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||||
|
cd package-manager
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
### Setup modes
|
### Setup modes
|
||||||
|
|
||||||
@@ -138,17 +208,8 @@ The script detects and normalizes the OS and installs the required **system-leve
|
|||||||
| **make setup** | Python venv **and** Nix | Full development & CI |
|
| **make setup** | Python venv **and** Nix | Full development & CI |
|
||||||
| **make setup-venv** | Python venv only | Local user setup |
|
| **make setup-venv** | Python venv only | Local user setup |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Install & setup
|
##### Full setup (venv + Nix)
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
|
||||||
cd package-manager
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Full setup (venv + Nix)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make setup
|
make setup
|
||||||
@@ -156,7 +217,7 @@ make setup
|
|||||||
|
|
||||||
Use this for CI, servers, containers and full development workflows.
|
Use this for CI, servers, containers and full development workflows.
|
||||||
|
|
||||||
#### Venv-only setup
|
##### Venv-only setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make setup-venv
|
make setup-venv
|
||||||
@@ -167,38 +228,116 @@ Use this if you want PKGMGR isolated without Nix integration.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Run without installation (Nix)
|
Alles klar 🙂
|
||||||
|
Hier ist der **RUN-Abschnitt ohne Gedankenstriche**, klar nach **Nix, Docker und venv** getrennt:
|
||||||
|
|
||||||
Run PKGMGR directly via Nix Flakes.
|
---
|
||||||
|
|
||||||
|
## Run PKGMGR 🧰
|
||||||
|
|
||||||
|
PKGMGR can be executed in different environments.
|
||||||
|
All modes expose the same CLI and commands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Run via Nix (no installation)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help
|
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
Example:
|
---
|
||||||
|
|
||||||
```bash
|
### Run via Docker 🐳
|
||||||
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- version pkgmgr
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
PKGMGR can be executed **inside Docker containers** for CI, testing and isolated
|
||||||
|
workflows.
|
||||||
* full flake URL required
|
|
||||||
* `--` separates Nix and PKGMGR arguments
|
|
||||||
* can be used alongside any setup mode
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage 🧰
|
#### Container images
|
||||||
|
|
||||||
After installation, the main entry point is:
|
One image per distribution, each installing PKGMGR as a distro-native package
|
||||||
|
on top of the matching base image from
|
||||||
|
[base-images](https://github.com/kevinveenbirkenbach/base-images). Every image
|
||||||
|
is a manifest list covering `linux/amd64` and `linux/arm64`.
|
||||||
|
|
||||||
|
All images live under `ghcr.io/kevinveenbirkenbach/`:
|
||||||
|
|
||||||
|
| Tag | Contains |
|
||||||
|
| ------------------------------------- | --------------------------------------------------------- |
|
||||||
|
| `pkgmgr-<distro>:latest` | newest build of the newest release on `main` |
|
||||||
|
| `pkgmgr-<distro>:stable` | newest build of the release the git tag `stable` marks |
|
||||||
|
| `pkgmgr-<distro>:<major>` | newest build of the newest release of that major |
|
||||||
|
| `pkgmgr-<distro>:<major>.<minor>` | newest build of the newest release of that minor |
|
||||||
|
| `pkgmgr-<distro>:<version>` | newest build of that release |
|
||||||
|
| `pkgmgr-<distro>:<version>.<unix-ts>` | one build, never moves |
|
||||||
|
|
||||||
|
`<distro>` is one of `arch`, `debian`, `ubuntu`, `fedora`, `centos`. The Arch
|
||||||
|
image is published without the suffix as well, so `pkgmgr:stable` and
|
||||||
|
`pkgmgr-arch:stable` are the same image.
|
||||||
|
|
||||||
|
`<version>` is the PKGMGR release and `<unix-ts>` the build time, so
|
||||||
|
`2.3.0.1767225600` is a full version an updater can order and pin. Every tag of
|
||||||
|
one build points at the same image.
|
||||||
|
|
||||||
|
`stable` moves only once "Mark stable commit" has moved the git tag `stable`,
|
||||||
|
which it does after every test of that release passed. Every day at 03:00 UTC,
|
||||||
|
after the daily 00:00 UTC build of base-images, and on demand through the
|
||||||
|
workflow's manual trigger, two releases are rebuilt on the newest base image:
|
||||||
|
|
||||||
|
- the newest `vX.Y.Z` release on `main`, moving `latest`, its prefixes and
|
||||||
|
`<version>`, and adding `<version>.<unix-ts>`;
|
||||||
|
- the release the git tag `stable` marks, when that is an older one, moving
|
||||||
|
`stable` and `<version>`, adding its own `<version>.<unix-ts>`, and moving a
|
||||||
|
prefix only while it is still the newest release of that prefix.
|
||||||
|
|
||||||
|
With newest `2.4.0` and stable `2.3.0`, `2.4.0` takes `latest`, `2` and `2.4`,
|
||||||
|
and `2.3.0` takes `stable` and `2.3`. A security update in base-images thereby
|
||||||
|
reaches whoever pinned a major, a minor, a release or follows `stable`.
|
||||||
|
|
||||||
|
Each release is compared with its own published `<version>`. A rebuild whose
|
||||||
|
installed packages equal it pushes nothing, since a new timestamp tag would make
|
||||||
|
every pin bump for no change; a tag of that release that does not show the
|
||||||
|
published build yet, such as `stable` right after "Mark stable commit", is moved
|
||||||
|
without a new timestamp.
|
||||||
|
|
||||||
|
A rebuild runs the build scripts of the release it rebuilds, not those of
|
||||||
|
`main`, so the skip and the tag rules above only hold for releases tagged after
|
||||||
|
this scheme landed. The scripts of an earlier release know no aliases: its merge
|
||||||
|
step fails and leaves its tags where they are, until a newer release replaces
|
||||||
|
it as the newest and the stable one.
|
||||||
|
|
||||||
|
For a clean environment that carries the system dependencies but no PKGMGR,
|
||||||
|
pull `base-<distro>` from
|
||||||
|
[base-images](https://github.com/kevinveenbirkenbach/base-images) directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Run examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -it \
|
||||||
|
-v "$PWD":/src \
|
||||||
|
-w /src \
|
||||||
|
ghcr.io/kevinveenbirkenbach/pkgmgr:stable \
|
||||||
|
pkgmgr --help
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Run via virtual environment (venv)
|
||||||
|
|
||||||
|
After activating the venv:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pkgmgr --help
|
pkgmgr --help
|
||||||
```
|
```
|
||||||
|
|
||||||
This prints a list of all available subcommands.
|
---
|
||||||
The help for each command is available via:
|
|
||||||
|
This allows you to choose between zero install execution using Nix, fully prebuilt
|
||||||
|
Docker environments or local isolated venv setups with identical command behavior.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+27
@@ -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
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
outputs = { self, nixpkgs }:
|
||||||
@@ -26,17 +26,13 @@
|
|||||||
packages = forAllSystems (system:
|
packages = forAllSystems (system:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
python = pkgs.python312;
|
||||||
# Single source of truth for pkgmgr: Python 3.11
|
pyPkgs = pkgs.python312Packages;
|
||||||
# - Matches pyproject.toml: requires-python = ">=3.11"
|
|
||||||
# - Uses python311Packages so that PyYAML etc. are available
|
|
||||||
python = pkgs.python311;
|
|
||||||
pyPkgs = pkgs.python311Packages;
|
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
pkgmgr = pyPkgs.buildPythonApplication {
|
pkgmgr = pyPkgs.buildPythonApplication {
|
||||||
pname = "package-manager";
|
pname = "package-manager";
|
||||||
version = "1.4.1";
|
version = "2.4.0";
|
||||||
|
|
||||||
# Use the git repo as source
|
# Use the git repo as source
|
||||||
src = ./.;
|
src = ./.;
|
||||||
@@ -44,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
|
||||||
@@ -53,7 +53,10 @@
|
|||||||
# Runtime dependencies (matches [project.dependencies] in pyproject.toml)
|
# Runtime dependencies (matches [project.dependencies] in pyproject.toml)
|
||||||
propagatedBuildInputs = [
|
propagatedBuildInputs = [
|
||||||
pyPkgs.pyyaml
|
pyPkgs.pyyaml
|
||||||
|
pyPkgs.jinja2
|
||||||
pyPkgs.pip
|
pyPkgs.pip
|
||||||
|
pkgs.git
|
||||||
|
pkgs.gnupg
|
||||||
];
|
];
|
||||||
|
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
@@ -76,12 +79,13 @@
|
|||||||
if pkgs ? ansible-core then pkgs.ansible-core
|
if pkgs ? ansible-core then pkgs.ansible-core
|
||||||
else pkgs.ansible;
|
else pkgs.ansible;
|
||||||
|
|
||||||
# Use the same Python version as the package (3.11)
|
# Use the same Python version as the package
|
||||||
python = pkgs.python311;
|
python = pkgs.python312;
|
||||||
|
|
||||||
pythonWithDeps = python.withPackages (ps: [
|
pythonWithDeps = python.withPackages (ps: [
|
||||||
ps.pip
|
ps.pip
|
||||||
ps.pyyaml
|
ps.pyyaml
|
||||||
|
ps.jinja2
|
||||||
]);
|
]);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -89,7 +93,11 @@
|
|||||||
buildInputs = [
|
buildInputs = [
|
||||||
pythonWithDeps
|
pythonWithDeps
|
||||||
pkgs.git
|
pkgs.git
|
||||||
|
pkgs.gnupg
|
||||||
ansiblePkg
|
ansiblePkg
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.openldap
|
||||||
|
pkgs.cyrus_sasl
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|||||||
+14
-4
@@ -1,15 +1,25 @@
|
|||||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||||
|
|
||||||
pkgname=package-manager
|
pkgname=package-manager
|
||||||
pkgver=0.9.1
|
pkgver=2.4.0
|
||||||
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')
|
||||||
url="https://github.com/kevinveenbirkenbach/package-manager"
|
url="https://github.com/kevinveenbirkenbach/package-manager"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
|
|
||||||
# Nix is the only runtime dependency; Python is provided by the Nix closure.
|
# Nix is required at runtime to run pkgmgr via the flake.
|
||||||
depends=('nix')
|
# On Arch x86_64 we can depend on the distro package.
|
||||||
|
# On other arches (e.g. ARM) we only declare it as optional because the
|
||||||
|
# repo package may be broken/out-of-sync; installation can be done via the official installer.
|
||||||
|
depends=()
|
||||||
|
optdepends=('nix: required to run pkgmgr via flake')
|
||||||
|
|
||||||
|
if [[ "${CARCH}" == "x86_64" ]]; then
|
||||||
|
depends=('nix')
|
||||||
|
optdepends=()
|
||||||
|
fi
|
||||||
|
|
||||||
makedepends=('rsync')
|
makedepends=('rsync')
|
||||||
|
|
||||||
install=${pkgname}.install
|
install=${pkgname}.install
|
||||||
@@ -47,7 +57,7 @@ package() {
|
|||||||
cd "$srcdir/$_srcdir_name"
|
cd "$srcdir/$_srcdir_name"
|
||||||
|
|
||||||
# Install the wrapper into /usr/bin
|
# Install the wrapper into /usr/bin
|
||||||
install -Dm0755 "scripts/pkgmgr-wrapper.sh" \
|
install -Dm0755 "scripts/launcher.sh" \
|
||||||
"$pkgdir/usr/bin/pkgmgr"
|
"$pkgdir/usr/bin/pkgmgr"
|
||||||
|
|
||||||
# Install Nix bootstrap (init + lib)
|
# Install Nix bootstrap (init + lib)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
post_install() {
|
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() {
|
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() {
|
post_remove() {
|
||||||
|
|||||||
@@ -1,3 +1,567 @@
|
|||||||
|
package-manager (2.4.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Worktree: *worktree up* and *down* give each branch its own slot
|
||||||
|
- Worktree: *worktree flow* keeps pull sources and push targets per worktree
|
||||||
|
- Worktree: *up --pull* and *--push* record the flow; default is the base
|
||||||
|
- Branch: *branch tree* writes a hardened meta-tree of all repositories
|
||||||
|
- Branch: the tree carries each worktree's merge flow
|
||||||
|
- GC: *gc --report* prints a read-only disk inventory
|
||||||
|
- Review: a branch review renders as a local HTML page
|
||||||
|
- Publish: releases rebuild nightly; stable follows the tested build
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
|
||||||
|
- Push: signs only an unsigned HEAD instead of re-signing the whole range
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Push, release, gc: refuse a repository config that would run a program
|
||||||
|
- Git: hooks named in the config no longer run in hardened git calls
|
||||||
|
- Tests: the trusted PATH fixture comes from os.defpath, not the host
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Push: move repo-local sshCommand, credential helpers, hooks to global config
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sat, 07 Nov 2026 15:45:28 +0000
|
||||||
|
|
||||||
|
package-manager (2.3.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Branch: *gc --merged-into REF* treats items merged into REF as merged
|
||||||
|
- Branch: *--merged-into* is repeatable, protects REF and works for *purge*
|
||||||
|
- Branch: *--sizes* adds each worktree's disk usage to the plan
|
||||||
|
- Branch: *--min-age HOURS* skips items with a commit or index that recent
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 01 Nov 2026 15:14:53 +0100
|
||||||
|
|
||||||
|
package-manager (2.2.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Push: *--no-sign* pushes unsigned commits as they are; leases still apply
|
||||||
|
- Push: *--rewrite* replaces commits a remote already has (amend, squash)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
|
||||||
|
- Push: *pkgmgr push* signs every unpushed unsigned commit before pushing
|
||||||
|
- Push: each pushurl gets its own lease; mirrors no longer fail as stale
|
||||||
|
- Push: a commit that exists only on a mirror stops the push, nothing lost
|
||||||
|
- Push: without a selection it pushes the current checkout or worktree
|
||||||
|
- Release: signs before tagging and pushes branch and tag like *pkgmgr push*
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Container: images build on native amd64 and arm64 runners, not QEMU
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Push: needs a working signing key; pass *--no-sign* to push unsigned
|
||||||
|
- Container: no 2.1.0 or 2.1.1 image reached GHCR; notes below carry over
|
||||||
|
- Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 01 Nov 2026 01:55:29 +0100
|
||||||
|
|
||||||
|
package-manager (2.1.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Release: tool-only or dynamic-version pyproject.toml is skipped, no crash
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Container: no 2.1.0 image reached GHCR; the two notes below carry over
|
||||||
|
- Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sat, 31 Oct 2026 18:47:35 +0100
|
||||||
|
|
||||||
|
package-manager (2.1.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Branch: *pkgmgr branch gc* removes merged clean branches, worktrees, stacks
|
||||||
|
- Branch: *gc* only prints its plan; *--apply* acts, *--json* for scripts
|
||||||
|
- Branch: *pkgmgr branch purge* deletes unmerged or dirty items after y/N
|
||||||
|
- Release: Cargo.toml version and its Cargo.lock entry follow the release
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
|
||||||
|
- Release: starts from the highest version in the tag and all version files
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sat, 31 Oct 2026 14:38:54 +0100
|
||||||
|
|
||||||
|
package-manager (2.0.5-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Config: *pkgmgr config edit* opens nano instead of aborting without a TTY
|
||||||
|
- Config: *pkgmgr config edit* exits with the editor's code when it fails
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 29 Oct 2026 13:57:31 +0100
|
||||||
|
|
||||||
|
package-manager (2.0.4-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- CI: Python lint passes again on ruff 0.17 and still on 0.16
|
||||||
|
- CI: actions bumped: *actions/checkout* v7, *hadolint-action* 3.5.0
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- 2.0.3 was never marked *stable* nor published to GHCR: its CI failed
|
||||||
|
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 19 Oct 2026 10:06:02 +0200
|
||||||
|
|
||||||
|
package-manager (2.0.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Release: the entry is linted inside the CHANGELOG.md it lands in
|
||||||
|
- Release: a list marker that breaks the file's consistency is caught (MD004)
|
||||||
|
- Release: findings the file already has are not blamed on the entry
|
||||||
|
- Release: *--no-globs* keeps the target repo's other files out of the lint
|
||||||
|
- Release: without markdownlint-cli2, list items must use dash bullets
|
||||||
|
|
||||||
|
Upgrade notes:
|
||||||
|
|
||||||
|
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 18 Oct 2026 23:33:52 +0200
|
||||||
|
|
||||||
|
package-manager (2.0.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Fixes a release flow that could reject every entry written for it.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- A heading in a changelog message became bold, which markdown-lint reads
|
||||||
|
back as a heading and refuses with MD036
|
||||||
|
- Since the editor reopens on failure, no structured entry could ever pass
|
||||||
|
- Headings now move below the release heading they are filed under
|
||||||
|
- The lint findings reached nobody: the editor took over the terminal in the
|
||||||
|
same breath they were printed
|
||||||
|
- They are held until Enter and repeated inside the editor, above the text
|
||||||
|
- The Makefile installer accepted install-* as proof of an install target but
|
||||||
|
always ran plain make install
|
||||||
|
- A repository with install-python and no install died on "No rule to make
|
||||||
|
target" instead of being skipped
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- transform and lint were covered separately, so a transform producing lint
|
||||||
|
errors went unseen; one test now runs the first through the second
|
||||||
|
- Three tests were checking behaviour the code had already left behind
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- docker/login-action 3.7.0 to 4.6.0, setup-buildx-action 3.12.0 to 4.4.0
|
||||||
|
- actions/setup-python 5 to 7
|
||||||
|
- DeterminateSystems nix-installer-action 16 to 23, update-flake-lock 24 to 29
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
- Nothing to do; a changelog entry written with headings now keeps them
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 01 Oct 2026 17:47:12 +0200
|
||||||
|
|
||||||
|
package-manager (2.0.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* A release no longer ends *CHANGELOG.md* on a blank line, which is MD012.
|
||||||
|
* The entry's separator only belongs there when another entry follows it.
|
||||||
|
* Affects the first entry a file receives, so it shows on an opening release.
|
||||||
|
* Four tests cover the placements in *_insert_after_h1*, one per branch.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 18 Sep 2026 17:15:35 +0200
|
||||||
|
|
||||||
|
package-manager (2.0.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
Breaking
|
||||||
|
|
||||||
|
* The *virgin* and *slim* image variants are gone; both tag suffixes with them.
|
||||||
|
* Building an image now requires the base images to exist in the registry.
|
||||||
|
* *make build-missing-virgin* is removed; *build* targets build the one image.
|
||||||
|
* The five *BASE_IMAGE_** overrides are gone; *BASE_IMAGES_TAG* replaces them.
|
||||||
|
* *scripts/build/image.sh* loses its *--target* axis.
|
||||||
|
* A repository is installed by one hook, no longer by pip and *make* both.
|
||||||
|
|
||||||
|
Changed
|
||||||
|
|
||||||
|
* *INSTALLERS* is a preference order led by *make install*, first success wins.
|
||||||
|
* A failed hook falls back to the next; all failing exits with what was tried.
|
||||||
|
* The *Dockerfile* builds *FROM ${BASE_IMAGE}*, resolved by *base.sh*.
|
||||||
|
* Publishing pushes *--platform* manifest lists for amd64 and arm64.
|
||||||
|
* *manjaro* can be built; *os_resolver.sh* already maps it onto arch.
|
||||||
|
* The virgin workflows pull the base image instead of building it.
|
||||||
|
* The Nix dev shell moves to Python 3.12 for *portfolio*'s *>=3.12* floor.
|
||||||
|
* It gains *pkg-config*, *openldap* and *cyrus_sasl* for *python-ldap*.
|
||||||
|
* *requires-python* for pkgmgr itself is unchanged at *>=3.9*.
|
||||||
|
* The e2e sweep drops *--silent*, which hid the non-zero exit, not the output.
|
||||||
|
* The README drops the *Virgin* type and the Nix claim the image never met.
|
||||||
|
* It now lists the tags *compute_publish_tags()* pushes, alias included.
|
||||||
|
* Dependabot covers pip and github-actions weekly on Monday.
|
||||||
|
* A new *update-flake-lock* workflow opens a PR for *flake.lock* the same day.
|
||||||
|
* *test_distro_dependency_scripts_install_gpg_tools* moves to base-images.
|
||||||
|
* *scripts/docker/slim.sh* is deleted with the stage that ran it.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 18 Sep 2026 16:29:32 +0200
|
||||||
|
|
||||||
|
package-manager (1.16.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* *pkgmgr repos create* now creates *main*-based repositories: *git init* is run with an explicit *-b main* instead of inheriting the host's *init.defaultBranch*, and a failed initial push no longer renames the branch back to *master*. *resolve_base_branch()* keeps its main → master fallback for existing repositories.
|
||||||
|
* Failures are no longer swallowed: 46 blind *except Exception* handlers were narrowed to the exception types the guarded code can actually raise (*OSError*, *UnicodeDecodeError*, TOML/YAML decode errors, *ValueError*, *ImportError*, *subprocess.SubprocessError*, *GitRunError*, *PackageNotFoundError*). Three handlers stay open by design — the per-repository loops in *install* and *update* are batch boundaries where one failing repository must not abort the run.
|
||||||
|
* Keyring backend errors now surface as the concrete *KeyringOperationError*, raised at the boundary in *KeyringTokenProvider.get/set* with the original exception preserved as *__cause__*, and kept distinct from *KeyringUnavailableError* ("no usable backend" vs. "backend present, call failed"). Both degradation paths are unchanged.
|
||||||
|
* The *code-scanning* report stamps *generated_at* and its fallback output directory in UTC instead of a naive local timestamp.
|
||||||
|
* New *make lint* target runs *ruff* on *src* and *tests* with the same invocation as the *lint-python* workflow, and *make test* now depends on it. Repository-wide formatting and typing modernisation (PEP 604 unions with *from __future__ import annotations*, builtin generics, shebangs stripped from 72 importable modules) — *ruff check src tests* passes.
|
||||||
|
* New *tests/integration/test_error_path_degradation.py* drives 44 of the narrowed handlers into their except branch with real injected faults and asserts the documented degradation.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 29 Jul 2026 16:41:21 +0200
|
||||||
|
|
||||||
|
package-manager (1.16.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* New *code-scanning* command: *pkgmgr code-scanning* downloads a repository's GitHub code scanning alerts and analysis metadata via the *gh* CLI into a timestamped directory (default */tmp/<repo>/code-scanner/<timestamp>*), together with a readable summary, for offline analysis.
|
||||||
|
* The release flow now lints and normalises the changelog message before writing it: a leading heading becomes bold, inline code becomes italic, and the entry is validated against the repository's markdown rules, re-prompting interactively until it is clean.
|
||||||
|
* The Debian changelog and the RPM *%changelog* now mirror the full multi-line message correctly — every change line is indented or dash-prefixed — fixing a package build failure where un-indented bodies broke *dpkg-buildpackage* and *rpmspec*; the changelog entry no longer gains an auto-inserted leading bullet.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 28 Jun 2026 16:54:16 +0200
|
||||||
|
|
||||||
|
package-manager (1.15.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Restore `infinito` as an alias for the infinito-nexus/core repository so `pkgmgr install infinito` (and friends) resolves again.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 28 May 2026 11:06:43 +0200
|
||||||
|
|
||||||
|
package-manager (1.15.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Insert pkgmgr release changelog entry under the H1 instead of above it. Fixes the markdownlint MD041 (first-line-h1) and MD012 (no-multiple-blanks) regressions that previously trashed every CHANGELOG.md after a release.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 28 May 2026 08:18:23 +0200
|
||||||
|
|
||||||
|
package-manager (1.15.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Add pkgmgr archive subcommand: promote fully-checked NNN-topic.md spec files into the directorys README Archive section and delete the source files. Lookup pattern, README path, and template handling are configurable. Extracted from infinito-nexus-core so every kpmx-managed repo gets the same archival flow.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 28 May 2026 07:56:07 +0200
|
||||||
|
|
||||||
|
package-manager (1.14.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Added
|
||||||
|
|
||||||
|
* New release --retry mode re-deploys the HEAD release without
|
||||||
|
re-tagging or modifying any files. It re-pushes the existing version
|
||||||
|
tag, re-aligns the floating latest tag, and (unless --no-publish)
|
||||||
|
re-runs publish. Use this to recover from a release whose post-tag
|
||||||
|
push or PyPI upload failed mid-flight. The release_type argument
|
||||||
|
becomes optional under --retry.
|
||||||
|
* New module pkgmgr.actions.release.retry hosts the retry_release
|
||||||
|
helper so the workflow orchestrator stays focused on the forward
|
||||||
|
path.
|
||||||
|
* RepoPaths now exposes a debian_control slot, discovered alongside
|
||||||
|
debian_changelog under both packaging/debian and the legacy debian
|
||||||
|
layout.
|
||||||
|
* pkgmgr.actions.release.package_name.resolve_package_name centralises
|
||||||
|
the distro-name lookup chain and is unit-tested under
|
||||||
|
tests/unit/pkgmgr/actions/release/test_package_name.py.
|
||||||
|
* tests/unit/pkgmgr/actions/release/test_retry.py covers routing,
|
||||||
|
idempotent push, latest-tag re-alignment, missing-tag error path,
|
||||||
|
and branch-detection fallback.
|
||||||
|
|
||||||
|
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 no longer silently flips the debian/changelog top entry and
|
||||||
|
the RPM changelog stanza to a new identifier. Those keep matching
|
||||||
|
the authoritative value in the packaging files, which is what apt,
|
||||||
|
pacman, and dnf index against.
|
||||||
|
|
||||||
|
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
|
||||||
|
on the next release.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 27 May 2026 20:53:14 +0200
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 21 Jan 2026 01:18:31 +0100
|
||||||
|
|
||||||
|
package-manager (1.10.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Automated release.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 20 Jan 2026 10:44:58 +0100
|
||||||
|
|
||||||
|
package-manager (1.9.5-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Release patch: improve git pull error diagnostics
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 16 Jan 2026 10:09:43 +0100
|
||||||
|
|
||||||
|
package-manager (1.9.4-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* fix(ci): replace sudo with su for user switching to avoid PAM failures in minimal container images
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 13 Jan 2026 14:48:50 +0100
|
||||||
|
|
||||||
|
package-manager (1.9.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Made the Nix dependency optional on non-x86_64 architectures to avoid broken Arch Linux ARM repository packages.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 07 Jan 2026 13:44:40 +0100
|
||||||
|
|
||||||
|
package-manager (1.9.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Default configuration files are now packaged and loaded correctly when no user config exists, while fully preserving custom user configurations.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 21 Dec 2025 15:30:22 +0100
|
||||||
|
|
||||||
|
package-manager (1.9.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fixed installation issues and improved loading of default configuration files.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 21 Dec 2025 13:38:58 +0100
|
||||||
|
|
||||||
|
package-manager (1.9.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * New ***mirror visibility*** command to set remote Git repositories to ***public*** or ***private***.
|
||||||
|
* New ***--public*** flag for ***mirror provision*** to create repositories and immediately make them public.
|
||||||
|
* All configured git mirrors are now provisioned.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sat, 20 Dec 2025 14:37:58 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.7-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * **Release version updates now correctly modify ***pyproject.toml*** files that follow PEP 621**, ensuring the ***[project].version*** field is updated as expected.
|
||||||
|
* **Invalid or incomplete ***pyproject.toml*** files are now handled gracefully** with clear error messages instead of abrupt process termination.
|
||||||
|
* **RPM spec files remain compatible during releases**: existing macros such as ***%{?dist}*** are preserved and no longer accidentally modified.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 19 Dec 2025 14:15:47 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.6-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Prevent Rate Limits during GitHub Nix Setups
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 17 Dec 2025 23:50:31 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.5-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * Clearer Git error handling, especially when a directory is not a Git repository.
|
||||||
|
* More reliable repository verification with improved commit and GPG signature checks.
|
||||||
|
* Better error messages and overall robustness when working with Git-based workflows.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 17 Dec 2025 22:15:48 +0100
|
||||||
|
|
||||||
|
package-manager (1.9.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Automated release.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 17 Dec 2025 22:10:31 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.4-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * Made pkgmgr’s base-layer role explicit by standardizing the Docker/CI mount path to *`/opt/src/pkgmgr`*.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 17 Dec 2025 11:20:16 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 16 Dec 2025 19:49:51 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 16 Dec 2025 19:22:41 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.
|
||||||
|
* Mirrors are now handled cleanly: only valid Git remotes are used for Git operations, while non-Git URLs (e.g. PyPI) are excluded, preventing broken or confusing repository configs.
|
||||||
|
* GitHub authentication is more robust: tokens are automatically resolved via the GitHub CLI (`gh`), invalid stored tokens are replaced, and interactive prompts occur only when necessary.
|
||||||
|
* Repository creation and release workflows are more reliable, producing cleaner Git configurations and more predictable version handling.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 16 Dec 2025 18:06:35 +0100
|
||||||
|
|
||||||
|
package-manager (1.8.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* *** New Features: ***
|
||||||
|
- **Silent Updates**: You can now use the `--silent` flag during installs and updates to suppress error messages for individual repositories and get a single summary at the end. This ensures the process continues even if some repositories fail, while still preserving interactive checks when not in silent mode.
|
||||||
|
- **Repository Scaffolding**: The process for creating new repositories has been improved. You can now use templates to scaffold repositories with a preview and automatic mirror setup.
|
||||||
|
|
||||||
|
*** Bug Fixes: ***
|
||||||
|
- **Pip Installation**: Pip is now installed automatically on all supported systems. This includes `python-pip` for Arch and `python3-pip` for CentOS, Debian, Fedora, and Ubuntu, ensuring that pip is available for Python package installations.
|
||||||
|
- **Pacman Keyring**: Fixed an issue on Arch Linux where package installation would fail due to missing keys. The pacman keyring is now properly initialized before installing packages.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 15 Dec 2025 13:37:42 +0100
|
||||||
|
|
||||||
|
package-manager (1.7.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * Git mirrors are now resolved consistently (origin → MIRRORS file → config → default).
|
||||||
|
* The `origin` remote is always enforced to use the primary URL for both fetch and push.
|
||||||
|
* Additional mirrors are added as extra push targets without duplication.
|
||||||
|
* Local and remote mirror setup behaves more predictably and consistently.
|
||||||
|
* Improved test coverage ensures stable origin and push URL handling.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 15 Dec 2025 00:53:26 +0100
|
||||||
|
|
||||||
|
package-manager (1.7.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Patched package-manager to kpmx to publish on pypi
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 14 Dec 2025 21:19:11 +0100
|
||||||
|
|
||||||
|
package-manager (1.7.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * New *pkgmgr publish* command to publish repository artifacts to PyPI based on the *MIRRORS* file.
|
||||||
|
* Automatically selects the current repository when no explicit selection is given.
|
||||||
|
* Publishes only when a semantic version tag is present on *HEAD*; otherwise skips with a clear info message.
|
||||||
|
* Supports non-interactive mode for CI environments via *--non-interactive*.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 14 Dec 2025 21:10:06 +0100
|
||||||
|
|
||||||
|
package-manager (1.6.4-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* * Improved reliability of Nix installs and updates, including automatic resolution of profile conflicts and better handling of GitHub 403 rate limits.
|
||||||
|
* More stable launcher behavior in packaged and virtual-env setups.
|
||||||
|
* Enhanced mirror and remote handling: repository owner/name are derived from URLs, with smoother provisioning and clearer credential handling.
|
||||||
|
* More reliable releases and artifacts due to safer CI behavior when no version tag is present.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 14 Dec 2025 19:33:07 +0100
|
||||||
|
|
||||||
|
package-manager (1.6.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* ***Fixed:*** Corrected repository path resolution so release and version logic consistently use the canonical packaging/* layout, preventing changelog and packaging files from being read or updated from incorrect locations.
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 14 Dec 2025 13:39:52 +0100
|
||||||
|
|
||||||
package-manager (0.9.1-1) unstable; urgency=medium
|
package-manager (0.9.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ set -e
|
|||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
configure)
|
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
|
esac
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ override_dh_auto_install:
|
|||||||
install -d debian/package-manager/usr/lib/package-manager
|
install -d debian/package-manager/usr/lib/package-manager
|
||||||
|
|
||||||
# Install wrapper
|
# Install wrapper
|
||||||
install -m0755 scripts/pkgmgr-wrapper.sh \
|
install -m0755 scripts/launcher.sh \
|
||||||
debian/package-manager/usr/bin/pkgmgr
|
debian/package-manager/usr/bin/pkgmgr
|
||||||
|
|
||||||
# Install Nix bootstrap (init + lib)
|
# Install Nix bootstrap (init + lib)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: package-manager
|
Name: package-manager
|
||||||
Version: 0.9.1
|
Version: 2.4.0
|
||||||
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
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ install -d %{buildroot}/usr/lib/package-manager
|
|||||||
cp -a . %{buildroot}/usr/lib/package-manager/
|
cp -a . %{buildroot}/usr/lib/package-manager/
|
||||||
|
|
||||||
# Wrapper
|
# Wrapper
|
||||||
install -m0755 scripts/pkgmgr-wrapper.sh %{buildroot}%{_bindir}/pkgmgr
|
install -m0755 scripts/launcher.sh %{buildroot}%{_bindir}/pkgmgr
|
||||||
|
|
||||||
# Nix bootstrap (init + lib)
|
# Nix bootstrap (init + lib)
|
||||||
install -d %{buildroot}/usr/lib/package-manager/nix
|
install -d %{buildroot}/usr/lib/package-manager/nix
|
||||||
@@ -62,7 +62,7 @@ rm -rf \
|
|||||||
%{buildroot}/usr/lib/package-manager/.gitkeep || true
|
%{buildroot}/usr/lib/package-manager/.gitkeep || true
|
||||||
|
|
||||||
%post
|
%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
|
%postun
|
||||||
echo ">>> package-manager removed. Nix itself was not removed."
|
echo ">>> package-manager removed. Nix itself was not removed."
|
||||||
@@ -74,6 +74,414 @@ echo ">>> package-manager removed. Nix itself was not removed."
|
|||||||
/usr/lib/package-manager/
|
/usr/lib/package-manager/
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Sat Nov 07 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.4.0-1
|
||||||
|
- Added:
|
||||||
|
|
||||||
|
- - Worktree: *worktree up* and *down* give each branch its own slot
|
||||||
|
- - Worktree: *worktree flow* keeps pull sources and push targets per worktree
|
||||||
|
- - Worktree: *up --pull* and *--push* record the flow; default is the base
|
||||||
|
- - Branch: *branch tree* writes a hardened meta-tree of all repositories
|
||||||
|
- - Branch: the tree carries each worktree's merge flow
|
||||||
|
- - GC: *gc --report* prints a read-only disk inventory
|
||||||
|
- - Review: a branch review renders as a local HTML page
|
||||||
|
- - Publish: releases rebuild nightly; stable follows the tested build
|
||||||
|
|
||||||
|
- Changed:
|
||||||
|
|
||||||
|
- - Push: signs only an unsigned HEAD instead of re-signing the whole range
|
||||||
|
|
||||||
|
- Fixed:
|
||||||
|
|
||||||
|
- - Push, release, gc: refuse a repository config that would run a program
|
||||||
|
- - Git: hooks named in the config no longer run in hardened git calls
|
||||||
|
- - Tests: the trusted PATH fixture comes from os.defpath, not the host
|
||||||
|
|
||||||
|
- Upgrade notes:
|
||||||
|
|
||||||
|
- - Push: move repo-local sshCommand, credential helpers, hooks to global config
|
||||||
|
|
||||||
|
* Sun Nov 01 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.3.0-1
|
||||||
|
- Added:
|
||||||
|
|
||||||
|
- - Branch: *gc --merged-into REF* treats items merged into REF as merged
|
||||||
|
- - Branch: *--merged-into* is repeatable, protects REF and works for *purge*
|
||||||
|
- - Branch: *--sizes* adds each worktree's disk usage to the plan
|
||||||
|
- - Branch: *--min-age HOURS* skips items with a commit or index that recent
|
||||||
|
|
||||||
|
* Sun Nov 01 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.2.0-1
|
||||||
|
- Added:
|
||||||
|
|
||||||
|
- - Push: *--no-sign* pushes unsigned commits as they are; leases still apply
|
||||||
|
- - Push: *--rewrite* replaces commits a remote already has (amend, squash)
|
||||||
|
|
||||||
|
- Changed:
|
||||||
|
|
||||||
|
- - Push: *pkgmgr push* signs every unpushed unsigned commit before pushing
|
||||||
|
- - Push: each pushurl gets its own lease; mirrors no longer fail as stale
|
||||||
|
- - Push: a commit that exists only on a mirror stops the push, nothing lost
|
||||||
|
- - Push: without a selection it pushes the current checkout or worktree
|
||||||
|
- - Release: signs before tagging and pushes branch and tag like *pkgmgr push*
|
||||||
|
|
||||||
|
- Fixed:
|
||||||
|
|
||||||
|
- - Container: images build on native amd64 and arm64 runners, not QEMU
|
||||||
|
|
||||||
|
- Upgrade notes:
|
||||||
|
|
||||||
|
- - Push: needs a working signing key; pass *--no-sign* to push unsigned
|
||||||
|
- - Container: no 2.1.0 or 2.1.1 image reached GHCR; notes below carry over
|
||||||
|
- - Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- - Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
* Sat Oct 31 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.1.1-1
|
||||||
|
- Fixed:
|
||||||
|
|
||||||
|
- - Release: tool-only or dynamic-version pyproject.toml is skipped, no crash
|
||||||
|
|
||||||
|
- Upgrade notes:
|
||||||
|
|
||||||
|
- - Container: no 2.1.0 image reached GHCR; the two notes below carry over
|
||||||
|
- - Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- - Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
* Sat Oct 31 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.1.0-1
|
||||||
|
- Added:
|
||||||
|
|
||||||
|
- - Branch: *pkgmgr branch gc* removes merged clean branches, worktrees, stacks
|
||||||
|
- - Branch: *gc* only prints its plan; *--apply* acts, *--json* for scripts
|
||||||
|
- - Branch: *pkgmgr branch purge* deletes unmerged or dirty items after y/N
|
||||||
|
- - Release: Cargo.toml version and its Cargo.lock entry follow the release
|
||||||
|
|
||||||
|
- Changed:
|
||||||
|
|
||||||
|
- - Release: starts from the highest version in the tag and all version files
|
||||||
|
|
||||||
|
- Upgrade notes:
|
||||||
|
|
||||||
|
- - Release: a version file ahead of the newest tag now raises the next version
|
||||||
|
- - Branch: *gc --apply* and *purge* refuse to run inside a PID sandbox
|
||||||
|
|
||||||
|
* Thu Oct 29 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.5-1
|
||||||
|
- Fixed:
|
||||||
|
|
||||||
|
- - Config: *pkgmgr config edit* opens nano instead of aborting without a TTY
|
||||||
|
- - Config: *pkgmgr config edit* exits with the editor's code when it fails
|
||||||
|
|
||||||
|
* Mon Oct 19 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.4-1
|
||||||
|
- Fixed:
|
||||||
|
|
||||||
|
- - CI: Python lint passes again on ruff 0.17 and still on 0.16
|
||||||
|
- - CI: actions bumped: *actions/checkout* v7, *hadolint-action* 3.5.0
|
||||||
|
|
||||||
|
- Upgrade notes:
|
||||||
|
|
||||||
|
- - 2.0.3 was never marked *stable* nor published to GHCR: its CI failed
|
||||||
|
- - Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||||
|
|
||||||
|
* Sun Oct 18 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.3-1
|
||||||
|
- Fixed:
|
||||||
|
|
||||||
|
- - Release: the entry is linted inside the CHANGELOG.md it lands in
|
||||||
|
- - Release: a list marker that breaks the file's consistency is caught (MD004)
|
||||||
|
- - Release: findings the file already has are not blamed on the entry
|
||||||
|
- - Release: *--no-globs* keeps the target repo's other files out of the lint
|
||||||
|
- - Release: without markdownlint-cli2, list items must use dash bullets
|
||||||
|
|
||||||
|
- Upgrade notes:
|
||||||
|
|
||||||
|
- - Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||||
|
|
||||||
|
* Thu Oct 01 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.2-1
|
||||||
|
- Fixes a release flow that could reject every entry written for it.
|
||||||
|
|
||||||
|
- ### Fixed
|
||||||
|
|
||||||
|
- - A heading in a changelog message became bold, which markdown-lint reads
|
||||||
|
- back as a heading and refuses with MD036
|
||||||
|
- - Since the editor reopens on failure, no structured entry could ever pass
|
||||||
|
- - Headings now move below the release heading they are filed under
|
||||||
|
- - The lint findings reached nobody: the editor took over the terminal in the
|
||||||
|
- same breath they were printed
|
||||||
|
- - They are held until Enter and repeated inside the editor, above the text
|
||||||
|
- - The Makefile installer accepted install-* as proof of an install target but
|
||||||
|
- always ran plain make install
|
||||||
|
- - A repository with install-python and no install died on "No rule to make
|
||||||
|
- target" instead of being skipped
|
||||||
|
|
||||||
|
- ### Tests
|
||||||
|
|
||||||
|
- - transform and lint were covered separately, so a transform producing lint
|
||||||
|
- errors went unseen; one test now runs the first through the second
|
||||||
|
- - Three tests were checking behaviour the code had already left behind
|
||||||
|
|
||||||
|
- ### Dependencies
|
||||||
|
|
||||||
|
- - docker/login-action 3.7.0 to 4.6.0, setup-buildx-action 3.12.0 to 4.4.0
|
||||||
|
- - actions/setup-python 5 to 7
|
||||||
|
- - DeterminateSystems nix-installer-action 16 to 23, update-flake-lock 24 to 29
|
||||||
|
|
||||||
|
- ### Upgrading
|
||||||
|
|
||||||
|
- - Nothing to do; a changelog entry written with headings now keeps them
|
||||||
|
|
||||||
|
* Fri Sep 18 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.1-1
|
||||||
|
- * A release no longer ends *CHANGELOG.md* on a blank line, which is MD012.
|
||||||
|
- * The entry's separator only belongs there when another entry follows it.
|
||||||
|
- * Affects the first entry a file receives, so it shows on an opening release.
|
||||||
|
- * Four tests cover the placements in *_insert_after_h1*, one per branch.
|
||||||
|
|
||||||
|
* Fri Sep 18 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.0-1
|
||||||
|
- Breaking
|
||||||
|
|
||||||
|
- * The *virgin* and *slim* image variants are gone; both tag suffixes with them.
|
||||||
|
- * Building an image now requires the base images to exist in the registry.
|
||||||
|
- * *make build-missing-virgin* is removed; *build* targets build the one image.
|
||||||
|
- * The five *BASE_IMAGE_** overrides are gone; *BASE_IMAGES_TAG* replaces them.
|
||||||
|
- * *scripts/build/image.sh* loses its *--target* axis.
|
||||||
|
- * A repository is installed by one hook, no longer by pip and *make* both.
|
||||||
|
|
||||||
|
- Changed
|
||||||
|
|
||||||
|
- * *INSTALLERS* is a preference order led by *make install*, first success wins.
|
||||||
|
- * A failed hook falls back to the next; all failing exits with what was tried.
|
||||||
|
- * The *Dockerfile* builds *FROM ${BASE_IMAGE}*, resolved by *base.sh*.
|
||||||
|
- * Publishing pushes *--platform* manifest lists for amd64 and arm64.
|
||||||
|
- * *manjaro* can be built; *os_resolver.sh* already maps it onto arch.
|
||||||
|
- * The virgin workflows pull the base image instead of building it.
|
||||||
|
- * The Nix dev shell moves to Python 3.12 for *portfolio*'s *>=3.12* floor.
|
||||||
|
- * It gains *pkg-config*, *openldap* and *cyrus_sasl* for *python-ldap*.
|
||||||
|
- * *requires-python* for pkgmgr itself is unchanged at *>=3.9*.
|
||||||
|
- * The e2e sweep drops *--silent*, which hid the non-zero exit, not the output.
|
||||||
|
- * The README drops the *Virgin* type and the Nix claim the image never met.
|
||||||
|
- * It now lists the tags *compute_publish_tags()* pushes, alias included.
|
||||||
|
- * Dependabot covers pip and github-actions weekly on Monday.
|
||||||
|
- * A new *update-flake-lock* workflow opens a PR for *flake.lock* the same day.
|
||||||
|
- * *test_distro_dependency_scripts_install_gpg_tools* moves to base-images.
|
||||||
|
- * *scripts/docker/slim.sh* is deleted with the stage that ran it.
|
||||||
|
|
||||||
|
* Wed Jul 29 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.16.1-1
|
||||||
|
- * *pkgmgr repos create* now creates *main*-based repositories: *git init* is run with an explicit *-b main* instead of inheriting the host's *init.defaultBranch*, and a failed initial push no longer renames the branch back to *master*. *resolve_base_branch()* keeps its main → master fallback for existing repositories.
|
||||||
|
- * Failures are no longer swallowed: 46 blind *except Exception* handlers were narrowed to the exception types the guarded code can actually raise (*OSError*, *UnicodeDecodeError*, TOML/YAML decode errors, *ValueError*, *ImportError*, *subprocess.SubprocessError*, *GitRunError*, *PackageNotFoundError*). Three handlers stay open by design — the per-repository loops in *install* and *update* are batch boundaries where one failing repository must not abort the run.
|
||||||
|
- * Keyring backend errors now surface as the concrete *KeyringOperationError*, raised at the boundary in *KeyringTokenProvider.get/set* with the original exception preserved as *__cause__*, and kept distinct from *KeyringUnavailableError* ("no usable backend" vs. "backend present, call failed"). Both degradation paths are unchanged.
|
||||||
|
- * The *code-scanning* report stamps *generated_at* and its fallback output directory in UTC instead of a naive local timestamp.
|
||||||
|
- * New *make lint* target runs *ruff* on *src* and *tests* with the same invocation as the *lint-python* workflow, and *make test* now depends on it. Repository-wide formatting and typing modernisation (PEP 604 unions with *from __future__ import annotations*, builtin generics, shebangs stripped from 72 importable modules) — *ruff check src tests* passes.
|
||||||
|
- * New *tests/integration/test_error_path_degradation.py* drives 44 of the narrowed handlers into their except branch with real injected faults and asserts the documented degradation.
|
||||||
|
|
||||||
|
* Sun Jun 28 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.16.0-1
|
||||||
|
- * New *code-scanning* command: *pkgmgr code-scanning* downloads a repository's GitHub code scanning alerts and analysis metadata via the *gh* CLI into a timestamped directory (default */tmp/<repo>/code-scanner/<timestamp>*), together with a readable summary, for offline analysis.
|
||||||
|
- * The release flow now lints and normalises the changelog message before writing it: a leading heading becomes bold, inline code becomes italic, and the entry is validated against the repository's markdown rules, re-prompting interactively until it is clean.
|
||||||
|
- * The Debian changelog and the RPM *%changelog* now mirror the full multi-line message correctly — every change line is indented or dash-prefixed — fixing a package build failure where un-indented bodies broke *dpkg-buildpackage* and *rpmspec*; the changelog entry no longer gains an auto-inserted leading bullet.
|
||||||
|
|
||||||
|
* Thu May 28 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.15.2-1
|
||||||
|
- Restore `infinito` as an alias for the infinito-nexus/core repository so `pkgmgr install infinito` (and friends) resolves again.
|
||||||
|
|
||||||
|
* Thu May 28 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.15.1-1
|
||||||
|
- Insert pkgmgr release changelog entry under the H1 instead of above it. Fixes the markdownlint MD041 (first-line-h1) and MD012 (no-multiple-blanks) regressions that previously trashed every CHANGELOG.md after a release.
|
||||||
|
|
||||||
|
* Thu May 28 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.15.0-1
|
||||||
|
- Add pkgmgr archive subcommand: promote fully-checked NNN-topic.md spec files into the directorys README Archive section and delete the source files. Lookup pattern, README path, and template handling are configurable. Extracted from infinito-nexus-core so every kpmx-managed repo gets the same archival flow.
|
||||||
|
|
||||||
|
* Wed May 27 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.14.0-1
|
||||||
|
- Added
|
||||||
|
|
||||||
|
* New release --retry mode re-deploys the HEAD release without
|
||||||
|
re-tagging or modifying any files. It re-pushes the existing version
|
||||||
|
tag, re-aligns the floating latest tag, and (unless --no-publish)
|
||||||
|
re-runs publish. Use this to recover from a release whose post-tag
|
||||||
|
push or PyPI upload failed mid-flight. The release_type argument
|
||||||
|
becomes optional under --retry.
|
||||||
|
* New module pkgmgr.actions.release.retry hosts the retry_release
|
||||||
|
helper so the workflow orchestrator stays focused on the forward
|
||||||
|
path.
|
||||||
|
* RepoPaths now exposes a debian_control slot, discovered alongside
|
||||||
|
debian_changelog under both packaging/debian and the legacy debian
|
||||||
|
layout.
|
||||||
|
* pkgmgr.actions.release.package_name.resolve_package_name centralises
|
||||||
|
the distro-name lookup chain and is unit-tested under
|
||||||
|
tests/unit/pkgmgr/actions/release/test_package_name.py.
|
||||||
|
* tests/unit/pkgmgr/actions/release/test_retry.py covers routing,
|
||||||
|
idempotent push, latest-tag re-alignment, missing-tag error path,
|
||||||
|
and branch-detection fallback.
|
||||||
|
|
||||||
|
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 no longer silently flips the debian/changelog top entry and
|
||||||
|
the RPM changelog stanza to a new identifier. Those keep matching
|
||||||
|
the authoritative value in the packaging files, which is what apt,
|
||||||
|
pacman, and dnf index against.
|
||||||
|
|
||||||
|
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
|
||||||
|
on the next release.
|
||||||
|
|
||||||
|
* 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
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
* Tue Jan 20 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.10.0-1
|
||||||
|
- Automated release.
|
||||||
|
|
||||||
|
* Fri Jan 16 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.9.5-1
|
||||||
|
- Release patch: improve git pull error diagnostics
|
||||||
|
|
||||||
|
* Tue Jan 13 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.9.4-1
|
||||||
|
- fix(ci): replace sudo with su for user switching to avoid PAM failures in minimal container images
|
||||||
|
|
||||||
|
* Wed Jan 07 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.9.3-1
|
||||||
|
- Made the Nix dependency optional on non-x86_64 architectures to avoid broken Arch Linux ARM repository packages.
|
||||||
|
|
||||||
|
* Sun Dec 21 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.9.2-1
|
||||||
|
- Default configuration files are now packaged and loaded correctly when no user config exists, while fully preserving custom user configurations.
|
||||||
|
|
||||||
|
* Sun Dec 21 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.9.1-1
|
||||||
|
- Fixed installation issues and improved loading of default configuration files.
|
||||||
|
|
||||||
|
* Sat Dec 20 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.9.0-1
|
||||||
|
- * New ***mirror visibility*** command to set remote Git repositories to ***public*** or ***private***.
|
||||||
|
* New ***--public*** flag for ***mirror provision*** to create repositories and immediately make them public.
|
||||||
|
* All configured git mirrors are now provisioned.
|
||||||
|
|
||||||
|
* Fri Dec 19 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.7-1
|
||||||
|
- * **Release version updates now correctly modify ***pyproject.toml*** files that follow PEP 621**, ensuring the ***[project].version*** field is updated as expected.
|
||||||
|
* **Invalid or incomplete ***pyproject.toml*** files are now handled gracefully** with clear error messages instead of abrupt process termination.
|
||||||
|
* **RPM spec files remain compatible during releases**: existing macros such as ***%{?dist}*** are preserved and no longer accidentally modified.
|
||||||
|
|
||||||
|
* Wed Dec 17 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.6-1
|
||||||
|
- Prevent Rate Limits during GitHub Nix Setups
|
||||||
|
|
||||||
|
* Wed Dec 17 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.5-1
|
||||||
|
- * Clearer Git error handling, especially when a directory is not a Git repository.
|
||||||
|
* More reliable repository verification with improved commit and GPG signature checks.
|
||||||
|
* Better error messages and overall robustness when working with Git-based workflows.
|
||||||
|
|
||||||
|
* Wed Dec 17 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.9.0-1
|
||||||
|
- Automated release.
|
||||||
|
|
||||||
|
* Wed Dec 17 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.4-1
|
||||||
|
- * Made pkgmgr’s base-layer role explicit by standardizing the Docker/CI mount path to *`/opt/src/pkgmgr`*.
|
||||||
|
|
||||||
|
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.3-1
|
||||||
|
- MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
|
||||||
|
|
||||||
|
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.2-1
|
||||||
|
- * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
|
||||||
|
|
||||||
|
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.1-1
|
||||||
|
- * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.
|
||||||
|
* Mirrors are now handled cleanly: only valid Git remotes are used for Git operations, while non-Git URLs (e.g. PyPI) are excluded, preventing broken or confusing repository configs.
|
||||||
|
* GitHub authentication is more robust: tokens are automatically resolved via the GitHub CLI (`gh`), invalid stored tokens are replaced, and interactive prompts occur only when necessary.
|
||||||
|
* Repository creation and release workflows are more reliable, producing cleaner Git configurations and more predictable version handling.
|
||||||
|
|
||||||
|
* Mon Dec 15 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.0-1
|
||||||
|
- *** New Features: ***
|
||||||
|
- **Silent Updates**: You can now use the `--silent` flag during installs and updates to suppress error messages for individual repositories and get a single summary at the end. This ensures the process continues even if some repositories fail, while still preserving interactive checks when not in silent mode.
|
||||||
|
- **Repository Scaffolding**: The process for creating new repositories has been improved. You can now use templates to scaffold repositories with a preview and automatic mirror setup.
|
||||||
|
|
||||||
|
*** Bug Fixes: ***
|
||||||
|
- **Pip Installation**: Pip is now installed automatically on all supported systems. This includes `python-pip` for Arch and `python3-pip` for CentOS, Debian, Fedora, and Ubuntu, ensuring that pip is available for Python package installations.
|
||||||
|
- **Pacman Keyring**: Fixed an issue on Arch Linux where package installation would fail due to missing keys. The pacman keyring is now properly initialized before installing packages.
|
||||||
|
|
||||||
|
* Mon Dec 15 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.7.2-1
|
||||||
|
- * Git mirrors are now resolved consistently (origin → MIRRORS file → config → default).
|
||||||
|
* The `origin` remote is always enforced to use the primary URL for both fetch and push.
|
||||||
|
* Additional mirrors are added as extra push targets without duplication.
|
||||||
|
* Local and remote mirror setup behaves more predictably and consistently.
|
||||||
|
* Improved test coverage ensures stable origin and push URL handling.
|
||||||
|
|
||||||
|
* Sun Dec 14 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.7.1-1
|
||||||
|
- Patched package-manager to kpmx to publish on pypi
|
||||||
|
|
||||||
|
* Sun Dec 14 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.7.0-1
|
||||||
|
- * New *pkgmgr publish* command to publish repository artifacts to PyPI based on the *MIRRORS* file.
|
||||||
|
* Automatically selects the current repository when no explicit selection is given.
|
||||||
|
* Publishes only when a semantic version tag is present on *HEAD*; otherwise skips with a clear info message.
|
||||||
|
* Supports non-interactive mode for CI environments via *--non-interactive*.
|
||||||
|
|
||||||
|
* Sun Dec 14 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.6.4-1
|
||||||
|
- * Improved reliability of Nix installs and updates, including automatic resolution of profile conflicts and better handling of GitHub 403 rate limits.
|
||||||
|
* More stable launcher behavior in packaged and virtual-env setups.
|
||||||
|
* Enhanced mirror and remote handling: repository owner/name are derived from URLs, with smoother provisioning and clearer credential handling.
|
||||||
|
* More reliable releases and artifacts due to safer CI behavior when no version tag is present.
|
||||||
|
|
||||||
|
* Sun Dec 14 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.6.3-1
|
||||||
|
- ***Fixed:*** Corrected repository path resolution so release and version logic consistently use the canonical packaging/* layout, preventing changelog and packaging files from being read or updated from incorrect locations.
|
||||||
|
|
||||||
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.9.1-1
|
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.9.1-1
|
||||||
- * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
- * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
||||||
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
|
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
|
||||||
|
|||||||
+13
-9
@@ -6,8 +6,8 @@ requires = [
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "package-manager"
|
name = "kpmx"
|
||||||
version = "1.4.1"
|
version = "2.4.0"
|
||||||
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"
|
||||||
@@ -19,16 +19,18 @@ authors = [
|
|||||||
|
|
||||||
# Base runtime dependencies
|
# Base runtime dependencies
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"PyYAML>=6.0"
|
"PyYAML>=6.0",
|
||||||
|
"tomli; python_version < \"3.11\"",
|
||||||
|
"jinja2>=3.1.6"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/kevinveenbirkenbach/package-manager"
|
Homepage = "https://s.veen.world/pkgmgr"
|
||||||
Source = "https://github.com/kevinveenbirkenbach/package-manager"
|
Source = "https://github.com/kevinveenbirkenbach/package-manager"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
keyring = ["keyring>=24.0.0"]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest",
|
|
||||||
"mypy"
|
"mypy"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -41,11 +43,13 @@ pkgmgr = "pkgmgr.cli:main"
|
|||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Source layout: all packages live under "src/"
|
# Source layout: all packages live under "src/"
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
package-dir = { "" = "src", "config" = "config" }
|
package-dir = { "" = "src" }
|
||||||
|
include-package-data = true
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src", "."]
|
where = ["src"]
|
||||||
include = ["pkgmgr*", "config*"]
|
include = ["pkgmgr*"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"config" = ["defaults.yaml"]
|
"pkgmgr.config" = ["*.yml", "*.yaml"]
|
||||||
|
"pkgmgr.actions.review" = ["*.j2"]
|
||||||
+46
-13
@@ -1,20 +1,53 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
: "${BASE_IMAGE_ARCH:=archlinux:latest}"
|
# The distribution bases this repository builds on. They are published by
|
||||||
: "${BASE_IMAGE_DEBIAN:=debian:stable-slim}"
|
# https://github.com/kevinveenbirkenbach/base-images, one multi-platform image
|
||||||
: "${BASE_IMAGE_UBUNTU:=ubuntu:latest}"
|
# per distribution, and carry the build dependencies the package build needs.
|
||||||
: "${BASE_IMAGE_FEDORA:=fedora:latest}"
|
#
|
||||||
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}"
|
# Env overrides exist for testing an unpublished base; the registry namespace
|
||||||
|
# follows the images repository, not this one.
|
||||||
|
|
||||||
|
: "${BASE_IMAGES_REGISTRY:=ghcr.io}"
|
||||||
|
: "${BASE_IMAGES_OWNER:=kevinveenbirkenbach}"
|
||||||
|
: "${BASE_IMAGES_TAG:=latest}"
|
||||||
|
|
||||||
resolve_base_image() {
|
resolve_base_image() {
|
||||||
local distro="$1"
|
local PKGMGR_DISTRO="$1"
|
||||||
case "$distro" in
|
case "$PKGMGR_DISTRO" in
|
||||||
arch) echo "$BASE_IMAGE_ARCH" ;;
|
arch|manjaro|debian|ubuntu|fedora|centos)
|
||||||
debian) echo "$BASE_IMAGE_DEBIAN" ;;
|
echo "${BASE_IMAGES_REGISTRY}/${BASE_IMAGES_OWNER}/base-${PKGMGR_DISTRO}:${BASE_IMAGES_TAG}"
|
||||||
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
|
;;
|
||||||
fedora) echo "$BASE_IMAGE_FEDORA" ;;
|
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
|
||||||
centos) echo "$BASE_IMAGE_CENTOS" ;;
|
|
||||||
*) echo "ERROR: Unknown distro '$distro'" >&2; exit 1 ;;
|
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Platforms each distribution's base offers; see base-images/scripts/build/distros.sh.
|
||||||
|
resolve_platforms() {
|
||||||
|
local PKGMGR_DISTRO="$1"
|
||||||
|
case "$PKGMGR_DISTRO" in
|
||||||
|
arch|manjaro|debian|ubuntu|fedora|centos) echo "linux/amd64,linux/arm64" ;;
|
||||||
|
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
package_set_cmd() {
|
||||||
|
local PKGMGR_DISTRO="$1"
|
||||||
|
case "$PKGMGR_DISTRO" in
|
||||||
|
arch|manjaro) echo 'pacman -Q' ;;
|
||||||
|
debian|ubuntu) echo 'dpkg-query -W' ;;
|
||||||
|
fedora|centos) echo 'rpm -qa | sort' ;;
|
||||||
|
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
version_prefixes() {
|
||||||
|
local -a parts
|
||||||
|
IFS=. read -ra parts <<< "$1"
|
||||||
|
local acc="${parts[0]}" i
|
||||||
|
echo "${acc}"
|
||||||
|
for ((i = 1; i < ${#parts[@]}; i++)); do
|
||||||
|
acc+=".${parts[i]}"
|
||||||
|
echo "${acc}"
|
||||||
|
done
|
||||||
|
}
|
||||||
+150
-67
@@ -2,50 +2,59 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/build/base.sh
|
||||||
source "${SCRIPT_DIR}/base.sh"
|
source "${SCRIPT_DIR}/base.sh"
|
||||||
|
|
||||||
: "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}"
|
: "${PKGMGR_DISTRO:?Environment variable 'PKGMGR_DISTRO' must be set (arch|debian|ubuntu|fedora|centos)}"
|
||||||
|
|
||||||
NO_CACHE=0
|
NO_CACHE=0
|
||||||
MISSING_ONLY=0
|
MISSING_ONLY=0
|
||||||
TARGET=""
|
|
||||||
IMAGE_TAG="" # local image name or base tag (without registry)
|
IMAGE_TAG="" # local image name or base tag (without registry)
|
||||||
PUSH=0 # if 1 -> use buildx and push (requires docker buildx)
|
PUSH=0 # if 1 -> use buildx and push (requires docker buildx)
|
||||||
PUBLISH=0 # if 1 -> push with semantic tags (latest/version/stable + arch aliases)
|
PUBLISH=0 # if 1 -> push <version>, <version>.<unix-ts> and the --aliases, plus arch aliases
|
||||||
REGISTRY="" # e.g. ghcr.io
|
REGISTRY="" # e.g. ghcr.io
|
||||||
OWNER="" # e.g. github org/user
|
OWNER="" # e.g. github org/user
|
||||||
REPO_PREFIX="pkgmgr" # image base name (pkgmgr)
|
REPO_PREFIX="pkgmgr" # image base name (pkgmgr)
|
||||||
VERSION="" # X.Y.Z (required for --publish)
|
VERSION="" # X.Y.Z (required for --publish)
|
||||||
IS_STABLE="false" # "true" -> publish stable tags
|
ALIASES="" # moving tags besides <version>, e.g. "latest 2 2.4 stable"
|
||||||
|
ALIASES_SET=0 # 1 once --aliases was given, also when empty
|
||||||
|
PLATFORM="" # e.g. linux/arm64 -> build and push only this platform
|
||||||
|
MERGE=0 # if 1 -> join the per-platform tags of --version under the publish tags
|
||||||
DEFAULT_DISTRO="arch"
|
DEFAULT_DISTRO="arch"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
local default_tag="pkgmgr-${distro}"
|
local default_tag="pkgmgr-${PKGMGR_DISTRO}"
|
||||||
if [[ -n "${TARGET:-}" ]]; then
|
|
||||||
default_tag="${default_tag}-${TARGET}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: distro=<distro> $0 [options]
|
Usage: PKGMGR_DISTRO=<distro> $0 [options]
|
||||||
|
|
||||||
Build options:
|
Build options:
|
||||||
--missing Build only if the image does not already exist (local build only)
|
--missing Build only if the image does not already exist (local build only)
|
||||||
--no-cache Build with --no-cache
|
--no-cache Build with --no-cache
|
||||||
--target <name> Build a specific Dockerfile target (e.g. virgin)
|
|
||||||
--tag <image> Override the output image tag (default: ${default_tag})
|
--tag <image> Override the output image tag (default: ${default_tag})
|
||||||
|
|
||||||
Publish options:
|
Publish options:
|
||||||
--push Push the built image (uses docker buildx build --push)
|
--push Push the built image (uses docker buildx build --push)
|
||||||
--publish Publish semantic tags (latest, <version>, optional stable) + arch aliases
|
--publish Publish <version>, <version>.<unix-ts> and every --aliases tag, plus arch
|
||||||
|
aliases
|
||||||
--registry <reg> Registry (e.g. ghcr.io)
|
--registry <reg> Registry (e.g. ghcr.io)
|
||||||
--owner <owner> Registry namespace (e.g. \${GITHUB_REPOSITORY_OWNER})
|
--owner <owner> Registry namespace (e.g. \${GITHUB_REPOSITORY_OWNER})
|
||||||
--repo-prefix <name> Image base name (default: pkgmgr)
|
--repo-prefix <name> Image base name (default: pkgmgr)
|
||||||
--version <X.Y.Z> Version for --publish
|
--version <X.Y.Z> Version for --publish
|
||||||
--stable <true|false> Whether to publish :stable tags (default: false)
|
--aliases "<tags>" Moving tags besides <version>, space separated, may be empty
|
||||||
|
(e.g. "latest 2 2.4 stable")
|
||||||
|
--platform <os/arch> Build and push only this platform; with --publish as <version>-<arch>,
|
||||||
|
skipped when its package set equals the published <version>
|
||||||
|
--merge Join the <version>-<arch> tags of all platforms under the publish tags;
|
||||||
|
when <version> already joins exactly these images, only the aliases
|
||||||
|
that do not are moved and no <version>.<unix-ts> is added
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- --publish implies --push and requires --registry, --owner, and --version.
|
- --publish implies --push and requires --registry, --owner, and --version.
|
||||||
- Local build (no --push) uses "docker build" and creates local images like "pkgmgr-arch" / "pkgmgr-arch-virgin".
|
- --publish without --platform requires --aliases.
|
||||||
|
- --platform requires --push; --merge requires --publish and builds nothing.
|
||||||
|
- Local build (no --push) uses "docker build" and creates a local image like "pkgmgr-arch".
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +62,6 @@ while [[ $# -gt 0 ]]; do
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
--no-cache) NO_CACHE=1; shift ;;
|
--no-cache) NO_CACHE=1; shift ;;
|
||||||
--missing) MISSING_ONLY=1; shift ;;
|
--missing) MISSING_ONLY=1; shift ;;
|
||||||
--target)
|
|
||||||
TARGET="${2:-}"
|
|
||||||
[[ -n "${TARGET}" ]] || { echo "ERROR: --target requires a value (e.g. virgin)"; exit 2; }
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--tag)
|
--tag)
|
||||||
IMAGE_TAG="${2:-}"
|
IMAGE_TAG="${2:-}"
|
||||||
[[ -n "${IMAGE_TAG}" ]] || { echo "ERROR: --tag requires a value"; exit 2; }
|
[[ -n "${IMAGE_TAG}" ]] || { echo "ERROR: --tag requires a value"; exit 2; }
|
||||||
@@ -85,11 +89,18 @@ while [[ $# -gt 0 ]]; do
|
|||||||
[[ -n "${VERSION}" ]] || { echo "ERROR: --version requires a value"; exit 2; }
|
[[ -n "${VERSION}" ]] || { echo "ERROR: --version requires a value"; exit 2; }
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--stable)
|
--aliases)
|
||||||
IS_STABLE="${2:-}"
|
[[ $# -ge 2 ]] || { echo "ERROR: --aliases requires a value, empty for none"; exit 2; }
|
||||||
[[ -n "${IS_STABLE}" ]] || { echo "ERROR: --stable requires a value (true|false)"; exit 2; }
|
ALIASES="$2"
|
||||||
|
ALIASES_SET=1
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--platform)
|
||||||
|
PLATFORM="${2:-}"
|
||||||
|
[[ -n "${PLATFORM}" ]] || { echo "ERROR: --platform requires a value"; exit 2; }
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--merge) MERGE=1; shift ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*)
|
*)
|
||||||
echo "ERROR: Unknown argument: $1" >&2
|
echo "ERROR: Unknown argument: $1" >&2
|
||||||
@@ -101,13 +112,10 @@ done
|
|||||||
|
|
||||||
# Derive default local tag if not provided
|
# Derive default local tag if not provided
|
||||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||||
IMAGE_TAG="${REPO_PREFIX}-${distro}"
|
IMAGE_TAG="${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||||
if [[ -n "${TARGET}" ]]; then
|
|
||||||
IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BASE_IMAGE="$(resolve_base_image "$distro")"
|
BASE_IMAGE="$(resolve_base_image "$PKGMGR_DISTRO")"
|
||||||
|
|
||||||
# Local-only "missing" shortcut
|
# Local-only "missing" shortcut
|
||||||
if [[ "${MISSING_ONLY}" == "1" ]]; then
|
if [[ "${MISSING_ONLY}" == "1" ]]; then
|
||||||
@@ -126,8 +134,24 @@ if [[ "${PUBLISH}" == "1" ]]; then
|
|||||||
[[ -n "${REGISTRY}" ]] || { echo "ERROR: --publish requires --registry"; exit 2; }
|
[[ -n "${REGISTRY}" ]] || { echo "ERROR: --publish requires --registry"; exit 2; }
|
||||||
[[ -n "${OWNER}" ]] || { echo "ERROR: --publish requires --owner"; exit 2; }
|
[[ -n "${OWNER}" ]] || { echo "ERROR: --publish requires --owner"; exit 2; }
|
||||||
[[ -n "${VERSION}" ]] || { echo "ERROR: --publish requires --version"; exit 2; }
|
[[ -n "${VERSION}" ]] || { echo "ERROR: --publish requires --version"; exit 2; }
|
||||||
|
[[ -n "${PLATFORM}" || "${ALIASES_SET}" == "1" ]] \
|
||||||
|
|| { echo "ERROR: --publish without --platform requires --aliases"; exit 2; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${PLATFORM}" ]]; then
|
||||||
|
[[ "${PUSH}" == "1" ]] || { echo "ERROR: --platform requires --push or --publish"; exit 2; }
|
||||||
|
[[ ",$(resolve_platforms "$PKGMGR_DISTRO")," == *",${PLATFORM},"* ]] \
|
||||||
|
|| { echo "ERROR: ${PKGMGR_DISTRO} offers no platform ${PLATFORM}"; exit 2; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${MERGE}" == "1" ]]; then
|
||||||
|
[[ "${PUBLISH}" == "1" ]] || { echo "ERROR: --merge requires --publish"; exit 2; }
|
||||||
|
[[ -z "${PLATFORM}" ]] || { echo "ERROR: --merge and --platform exclude each other"; exit 2; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
DISTRO_TAG_BASE="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||||
|
STAMP="$(date -u +%s)"
|
||||||
|
|
||||||
# Guard: --push without --publish requires fully-qualified --tag
|
# Guard: --push without --publish requires fully-qualified --tag
|
||||||
if [[ "${PUSH}" == "1" && "${PUBLISH}" != "1" ]]; then
|
if [[ "${PUSH}" == "1" && "${PUBLISH}" != "1" ]]; then
|
||||||
if [[ "${IMAGE_TAG}" != */* ]]; then
|
if [[ "${IMAGE_TAG}" != */* ]]; then
|
||||||
@@ -139,9 +163,9 @@ fi
|
|||||||
echo
|
echo
|
||||||
echo "------------------------------------------------------------"
|
echo "------------------------------------------------------------"
|
||||||
echo "[build] Building image"
|
echo "[build] Building image"
|
||||||
echo "distro = ${distro}"
|
echo "distro = ${PKGMGR_DISTRO}"
|
||||||
echo "BASE_IMAGE = ${BASE_IMAGE}"
|
echo "BASE_IMAGE = ${BASE_IMAGE}"
|
||||||
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
|
echo "platforms = ${PLATFORM:-$(resolve_platforms "$PKGMGR_DISTRO")}"
|
||||||
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
|
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
|
||||||
if [[ "${PUSH}" == "1" ]]; then echo "push = enabled"; fi
|
if [[ "${PUSH}" == "1" ]]; then echo "push = enabled"; fi
|
||||||
if [[ "${PUBLISH}" == "1" ]]; then
|
if [[ "${PUBLISH}" == "1" ]]; then
|
||||||
@@ -149,7 +173,8 @@ if [[ "${PUBLISH}" == "1" ]]; then
|
|||||||
echo "registry = ${REGISTRY}"
|
echo "registry = ${REGISTRY}"
|
||||||
echo "owner = ${OWNER}"
|
echo "owner = ${OWNER}"
|
||||||
echo "version = ${VERSION}"
|
echo "version = ${VERSION}"
|
||||||
echo "stable = ${IS_STABLE}"
|
echo "stamp = ${STAMP}"
|
||||||
|
echo "aliases = ${ALIASES}"
|
||||||
fi
|
fi
|
||||||
echo "------------------------------------------------------------"
|
echo "------------------------------------------------------------"
|
||||||
|
|
||||||
@@ -160,55 +185,113 @@ if [[ "${NO_CACHE}" == "1" ]]; then
|
|||||||
build_args+=(--no-cache)
|
build_args+=(--no-cache)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "${TARGET}" ]]; then
|
publish_bases() {
|
||||||
build_args+=(--target "${TARGET}")
|
echo "${DISTRO_TAG_BASE}"
|
||||||
fi
|
if [[ "${PKGMGR_DISTRO}" == "${DEFAULT_DISTRO}" ]]; then
|
||||||
|
echo "${REGISTRY}/${OWNER}/${REPO_PREFIX}"
|
||||||
compute_publish_tags() {
|
|
||||||
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${distro}"
|
|
||||||
local alias_tag_base=""
|
|
||||||
|
|
||||||
if [[ -n "${TARGET}" ]]; then
|
|
||||||
distro_tag_base="${distro_tag_base}-${TARGET}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${distro}" == "${DEFAULT_DISTRO}" ]]; then
|
|
||||||
alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}"
|
|
||||||
if [[ -n "${TARGET}" ]]; then
|
|
||||||
alias_tag_base="${alias_tag_base}-${TARGET}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
local tags=()
|
|
||||||
tags+=("${distro_tag_base}:latest")
|
|
||||||
tags+=("${distro_tag_base}:${VERSION}")
|
|
||||||
|
|
||||||
if [[ "${IS_STABLE}" == "true" ]]; then
|
|
||||||
tags+=("${distro_tag_base}:stable")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${alias_tag_base}" ]]; then
|
|
||||||
tags+=("${alias_tag_base}:latest")
|
|
||||||
tags+=("${alias_tag_base}:${VERSION}")
|
|
||||||
if [[ "${IS_STABLE}" == "true" ]]; then
|
|
||||||
tags+=("${alias_tag_base}:stable")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "${tags[@]}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${PUSH}" == "1" ]]; then
|
moving_tags() {
|
||||||
|
local base alias
|
||||||
|
local -a aliases
|
||||||
|
read -ra aliases <<< "${ALIASES}"
|
||||||
|
while read -r base; do
|
||||||
|
echo "${base}:${VERSION}"
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
echo "${base}:${alias}"
|
||||||
|
done
|
||||||
|
done < <(publish_bases)
|
||||||
|
}
|
||||||
|
|
||||||
|
stamp_tags() {
|
||||||
|
local base
|
||||||
|
while read -r base; do
|
||||||
|
echo "${base}:${VERSION}.${STAMP}"
|
||||||
|
done < <(publish_bases)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_index() {
|
||||||
|
docker buildx imagetools inspect --raw "$1" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
digest_of() {
|
||||||
|
docker buildx imagetools inspect --format '{{json .Manifest.Digest}}' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${MERGE}" == "1" ]]; then
|
||||||
|
sources=()
|
||||||
|
IFS=',' read -ra platforms <<< "$(resolve_platforms "$PKGMGR_DISTRO")"
|
||||||
|
for p in "${platforms[@]}"; do
|
||||||
|
sources+=("${DISTRO_TAG_BASE}:${VERSION}-${p#*/}")
|
||||||
|
done
|
||||||
|
|
||||||
|
mapfile -t moving < <(moving_tags)
|
||||||
|
planned="$(docker buildx imagetools create --dry-run -t "${DISTRO_TAG_BASE}:${VERSION}" "${sources[@]}")"
|
||||||
|
tags=()
|
||||||
|
if [[ "$(raw_index "${DISTRO_TAG_BASE}:${VERSION}")" == "${planned}" ]]; then
|
||||||
|
for t in "${moving[@]}"; do
|
||||||
|
if [[ "$(raw_index "${t}")" != "${planned}" ]]; then
|
||||||
|
tags+=("${t}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
mapfile -t stamps < <(stamp_tags)
|
||||||
|
tags=("${moving[@]}" "${stamps[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((${#tags[@]} == 0)); then
|
||||||
|
echo "[build] ${PKGMGR_DISTRO} ${VERSION}: every tag already joins these platform images, nothing to push."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
create_args=()
|
||||||
|
for t in "${tags[@]}"; do
|
||||||
|
create_args+=(-t "$t")
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[build] Running: docker buildx imagetools create ${create_args[*]} ${sources[*]}"
|
||||||
|
docker buildx imagetools create "${create_args[@]}" "${sources[@]}"
|
||||||
|
|
||||||
|
version_digest="$(digest_of "${DISTRO_TAG_BASE}:${VERSION}")"
|
||||||
|
for t in "${moving[@]}"; do
|
||||||
|
if [[ "$(digest_of "${t}")" != "${version_digest}" ]]; then
|
||||||
|
echo "[build] ❌ ${PKGMGR_DISTRO}: :${VERSION} and ${t} point at different images." >&2
|
||||||
|
echo "[build] Whoever pinned ${t##*:} stopped receiving what ${VERSION} gets." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "[build] ✅ ${PKGMGR_DISTRO}: every tag of ${VERSION} == ${version_digest}"
|
||||||
|
elif [[ "${PUSH}" == "1" ]]; then
|
||||||
|
if [[ "${PUBLISH}" == "1" && -n "${PLATFORM}" ]]; then
|
||||||
|
probe="${REPO_PREFIX}-${PKGMGR_DISTRO}:probe"
|
||||||
|
docker buildx build --load --platform "${PLATFORM}" -t "${probe}" "${build_args[@]}" .
|
||||||
|
|
||||||
|
package_set="$(package_set_cmd "${PKGMGR_DISTRO}")"
|
||||||
|
built_set="$(docker run --rm --platform "${PLATFORM}" --entrypoint bash "${probe}" -lc "${package_set}" | sha256sum)"
|
||||||
|
published_set="$(docker run --rm --pull always --platform "${PLATFORM}" --entrypoint bash "${DISTRO_TAG_BASE}:${VERSION}" -lc "${package_set}" 2>/dev/null | sha256sum || true)"
|
||||||
|
if [[ "${built_set}" == "${published_set}" ]]; then
|
||||||
|
echo "[build] ${PKGMGR_DISTRO} ${PLATFORM} unchanged since the published ${VERSION}, nothing to push."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
bx_args=(docker buildx build --push)
|
bx_args=(docker buildx build --push)
|
||||||
|
|
||||||
if [[ "${PUBLISH}" == "1" ]]; then
|
if [[ "${PUBLISH}" == "1" && -n "${PLATFORM}" ]]; then
|
||||||
|
bx_args+=(-t "${DISTRO_TAG_BASE}:${VERSION}-${PLATFORM#*/}")
|
||||||
|
elif [[ "${PUBLISH}" == "1" ]]; then
|
||||||
while IFS= read -r t; do
|
while IFS= read -r t; do
|
||||||
bx_args+=(-t "$t")
|
bx_args+=(-t "$t")
|
||||||
done < <(compute_publish_tags)
|
done < <(moving_tags)
|
||||||
|
while IFS= read -r t; do
|
||||||
|
bx_args+=(-t "$t")
|
||||||
|
done < <(stamp_tags)
|
||||||
else
|
else
|
||||||
bx_args+=(-t "${IMAGE_TAG}")
|
bx_args+=(-t "${IMAGE_TAG}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
bx_args+=(--platform "${PLATFORM:-$(resolve_platforms "$PKGMGR_DISTRO")}")
|
||||||
bx_args+=("${build_args[@]}")
|
bx_args+=("${build_args[@]}")
|
||||||
bx_args+=(.)
|
bx_args+=(.)
|
||||||
|
|
||||||
|
|||||||
+20
-16
@@ -1,54 +1,58 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Publish all distro images (full + virgin) to a registry via image.sh --publish
|
# Publish one image per distro, each covering every platform its base offers.
|
||||||
#
|
#
|
||||||
# Required env:
|
# Required env:
|
||||||
# OWNER (e.g. GITHUB_REPOSITORY_OWNER)
|
# OWNER (e.g. GITHUB_REPOSITORY_OWNER)
|
||||||
# VERSION (e.g. 1.2.3)
|
# VERSION (e.g. 1.2.3)
|
||||||
|
# ALIASES moving tags besides VERSION, space separated, may be empty (e.g. "latest 1 1.2 stable")
|
||||||
#
|
#
|
||||||
# Optional env:
|
# Optional env:
|
||||||
# REGISTRY (default: ghcr.io)
|
# REGISTRY (default: ghcr.io)
|
||||||
# IS_STABLE (default: false)
|
|
||||||
# DISTROS (default: "arch debian ubuntu fedora centos")
|
# DISTROS (default: "arch debian ubuntu fedora centos")
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/build/base.sh
|
||||||
|
source "${SCRIPT_DIR}/base.sh"
|
||||||
|
|
||||||
REGISTRY="${REGISTRY:-ghcr.io}"
|
REGISTRY="${REGISTRY:-ghcr.io}"
|
||||||
IS_STABLE="${IS_STABLE:-false}"
|
|
||||||
DISTROS="${DISTROS:-arch debian ubuntu fedora centos}"
|
DISTROS="${DISTROS:-arch debian ubuntu fedora centos}"
|
||||||
|
|
||||||
: "${OWNER:?Environment variable OWNER must be set (e.g. github.repository_owner)}"
|
: "${OWNER:?Environment variable OWNER must be set (e.g. github.repository_owner)}"
|
||||||
: "${VERSION:?Environment variable VERSION must be set (e.g. 1.2.3)}"
|
: "${VERSION:?Environment variable VERSION must be set (e.g. 1.2.3)}"
|
||||||
|
: "${ALIASES?Environment variable ALIASES must be set, empty for none (e.g. \"latest 1 1.2 stable\")}"
|
||||||
|
|
||||||
echo "[publish] REGISTRY=${REGISTRY}"
|
echo "[publish] REGISTRY=${REGISTRY}"
|
||||||
echo "[publish] OWNER=${OWNER}"
|
echo "[publish] OWNER=${OWNER}"
|
||||||
echo "[publish] VERSION=${VERSION}"
|
echo "[publish] VERSION=${VERSION}"
|
||||||
echo "[publish] IS_STABLE=${IS_STABLE}"
|
echo "[publish] ALIASES=${ALIASES}"
|
||||||
echo "[publish] DISTROS=${DISTROS}"
|
echo "[publish] DISTROS=${DISTROS}"
|
||||||
|
|
||||||
for d in ${DISTROS}; do
|
for d in ${DISTROS}; do
|
||||||
echo
|
echo
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo "[publish] distro=${d}"
|
echo "[publish] PKGMGR_DISTRO=${d}"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
# virgin
|
IFS=',' read -ra platforms <<< "$(resolve_platforms "${d}")"
|
||||||
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
for p in "${platforms[@]}"; do
|
||||||
--publish \
|
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||||
--registry "${REGISTRY}" \
|
--publish \
|
||||||
--owner "${OWNER}" \
|
--registry "${REGISTRY}" \
|
||||||
--version "${VERSION}" \
|
--owner "${OWNER}" \
|
||||||
--stable "${IS_STABLE}" \
|
--version "${VERSION}" \
|
||||||
--target virgin
|
--platform "${p}"
|
||||||
|
done
|
||||||
|
|
||||||
# full (default target)
|
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||||
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
|
||||||
--publish \
|
--publish \
|
||||||
--registry "${REGISTRY}" \
|
--registry "${REGISTRY}" \
|
||||||
--owner "${OWNER}" \
|
--owner "${OWNER}" \
|
||||||
--version "${VERSION}" \
|
--version "${VERSION}" \
|
||||||
--stable "${IS_STABLE}"
|
--aliases "${ALIASES}" \
|
||||||
|
--merge
|
||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
+10
-12
@@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
echo "[docker-pkgmgr] Starting package-manager container"
|
||||||
|
|
||||||
echo "[docker] Starting package-manager container"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Log distribution info
|
# Log distribution info
|
||||||
@@ -11,19 +9,19 @@ echo "[docker] Starting package-manager container"
|
|||||||
if [[ -f /etc/os-release ]]; then
|
if [[ -f /etc/os-release ]]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
echo "[docker] Detected distro: ${ID:-unknown} (like: ${ID_LIKE:-})"
|
echo "[docker-pkgmgr] Detected distro: ${ID:-unknown} (like: ${ID_LIKE:-})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Always use /src (mounted from host) as working directory
|
# Always use /opt/src/pkgmgr (mounted from host) as working directory
|
||||||
echo "[docker] Using /src as working directory"
|
echo "[docker-pkgmgr] Using /opt/src/pkgmgr as working directory"
|
||||||
cd /src
|
cd /opt/src/pkgmgr
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# DEV mode: rebuild package-manager from the mounted /src tree
|
# DEV mode: rebuild package-manager from the mounted /opt/src/pkgmgr tree
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ "${REINSTALL_PKGMGR:-0}" == "1" ]]; then
|
if [[ "${REINSTALL_PKGMGR:-0}" == "1" ]]; then
|
||||||
echo "[docker] DEV mode enabled (REINSTALL_PKGMGR=1)"
|
echo "[docker-pkgmgr] DEV mode enabled (REINSTALL_PKGMGR=1)"
|
||||||
echo "[docker] Rebuilding package-manager from /src via scripts/installation/package.sh..."
|
echo "[docker-pkgmgr] Rebuilding package-manager from /opt/src/pkgmgr via scripts/installation/package.sh..."
|
||||||
bash scripts/installation/package.sh || exit 1
|
bash scripts/installation/package.sh || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -31,9 +29,9 @@ fi
|
|||||||
# Hand off to pkgmgr or arbitrary command
|
# Hand off to pkgmgr or arbitrary command
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ $# -eq 0 ]]; then
|
if [[ $# -eq 0 ]]; then
|
||||||
echo "[docker] No arguments provided. Showing pkgmgr help..."
|
echo "[docker-pkgmgr] No arguments provided. Showing pkgmgr help..."
|
||||||
exec pkgmgr --help
|
exec pkgmgr --help
|
||||||
else
|
else
|
||||||
echo "[docker] Executing command: $*"
|
echo "[docker-pkgmgr] Executing command: $*"
|
||||||
exec "$@"
|
exec "$@"
|
||||||
fi
|
fi
|
||||||
@@ -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,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Param: WORKFLOW_RUN_SHA commit the "Mark stable commit" run tagged; empty on a scheduled or
|
||||||
|
# manual rebuild, which publishes the newest vX.Y.Z release on origin/main
|
||||||
|
# and the release the git tag stable marks
|
||||||
|
# Output: releases=<JSON list of {"sha", "version", "aliases"}> in GITHUB_OUTPUT; aliases are the
|
||||||
|
# moving tags a release takes besides <version> and <version>.<unix-ts>
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# shellcheck source=scripts/build/base.sh
|
||||||
|
source "${SCRIPT_DIR}/../../build/base.sh"
|
||||||
|
|
||||||
|
: "${WORKFLOW_RUN_SHA?WORKFLOW_RUN_SHA must be set, empty for a rebuild}"
|
||||||
|
: "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set}"
|
||||||
|
|
||||||
|
RELEASE_TAG='^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
|
||||||
|
release_at() {
|
||||||
|
local listed tag
|
||||||
|
listed="$(git tag --points-at "$1" --sort=-v:refname)"
|
||||||
|
tag="$(grep -m1 -E "${RELEASE_TAG}" <<< "${listed}" || true)"
|
||||||
|
echo "${tag#v}"
|
||||||
|
}
|
||||||
|
|
||||||
|
newest_of() {
|
||||||
|
local prefix="$1" version
|
||||||
|
for version in "${RELEASES[@]}"; do
|
||||||
|
if [[ "${version}." == "${prefix}."* ]]; then
|
||||||
|
echo "${version}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases_of() {
|
||||||
|
local version="$1" prefix
|
||||||
|
local -a aliases=()
|
||||||
|
if [[ "${version}" == "${RELEASES[0]}" ]]; then
|
||||||
|
aliases+=(latest)
|
||||||
|
fi
|
||||||
|
while read -r prefix; do
|
||||||
|
if [[ "${prefix}" != "${version}" && "$(newest_of "${prefix}")" == "${version}" ]]; then
|
||||||
|
aliases+=("${prefix}")
|
||||||
|
fi
|
||||||
|
done < <(version_prefixes "${version}")
|
||||||
|
if [[ "${version}" == "${STABLE}" ]]; then
|
||||||
|
aliases+=(stable)
|
||||||
|
fi
|
||||||
|
echo "${aliases[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
listed="$(git tag --merged origin/main --sort=-v:refname)"
|
||||||
|
mapfile -t tags < <(grep -E "${RELEASE_TAG}" <<< "${listed}")
|
||||||
|
RELEASES=("${tags[@]#v}")
|
||||||
|
|
||||||
|
STABLE=""
|
||||||
|
stable_sha="$(git rev-parse -q --verify 'refs/tags/stable^{commit}' || true)"
|
||||||
|
if [[ -n "${stable_sha}" ]]; then
|
||||||
|
STABLE="$(release_at "${stable_sha}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
targets=()
|
||||||
|
if [[ -n "${WORKFLOW_RUN_SHA}" ]]; then
|
||||||
|
version="$(release_at "${WORKFLOW_RUN_SHA}")"
|
||||||
|
if ! git merge-base --is-ancestor "${WORKFLOW_RUN_SHA}" origin/main; then
|
||||||
|
echo "${WORKFLOW_RUN_SHA} is not on origin/main. Skipping publish."
|
||||||
|
elif [[ -z "${version}" ]]; then
|
||||||
|
echo "No vX.Y.Z tag found for ${WORKFLOW_RUN_SHA}. Skipping publish."
|
||||||
|
else
|
||||||
|
targets+=("${version}")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
((${#RELEASES[@]} > 0)) || { echo "No vX.Y.Z tag is reachable from origin/main." >&2; exit 1; }
|
||||||
|
targets+=("${RELEASES[0]}")
|
||||||
|
if [[ -z "${STABLE}" ]]; then
|
||||||
|
echo "The git tag stable marks no vX.Y.Z release; the stable image stays where it is."
|
||||||
|
elif [[ "${STABLE}" != "${RELEASES[0]}" ]]; then
|
||||||
|
targets+=("${STABLE}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
json=""
|
||||||
|
for version in "${targets[@]}"; do
|
||||||
|
sha="$(git rev-parse "v${version}^{commit}")"
|
||||||
|
moving="$(aliases_of "${version}")"
|
||||||
|
echo "Publishing ${version} at ${sha}, moving: ${moving}"
|
||||||
|
json+="${json:+,}{\"sha\":\"${sha}\",\"version\":\"${version}\",\"aliases\":\"${moving}\"}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "releases=[${json}]" >> "${GITHUB_OUTPUT}"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${OWNER:?OWNER must be set}"
|
||||||
|
: "${VERSION:?VERSION must be set}"
|
||||||
|
: "${ALIASES?ALIASES must be set, empty for none}"
|
||||||
|
: "${PKGMGR_DISTRO:?PKGMGR_DISTRO must be set}"
|
||||||
|
|
||||||
|
bash scripts/build/image.sh \
|
||||||
|
--publish \
|
||||||
|
--registry ghcr.io \
|
||||||
|
--owner "${OWNER}" \
|
||||||
|
--version "${VERSION}" \
|
||||||
|
--aliases "${ALIASES}" \
|
||||||
|
--merge
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${OWNER:?OWNER must be set}"
|
||||||
|
: "${VERSION:?VERSION must be set}"
|
||||||
|
: "${PKGMGR_DISTRO:?PKGMGR_DISTRO must be set}"
|
||||||
|
: "${PLATFORM:?PLATFORM must be set}"
|
||||||
|
|
||||||
|
bash scripts/build/image.sh \
|
||||||
|
--publish \
|
||||||
|
--registry ghcr.io \
|
||||||
|
--owner "${OWNER}" \
|
||||||
|
--version "${VERSION}" \
|
||||||
|
--platform "${PLATFORM}"
|
||||||
@@ -38,11 +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
|
||||||
|
|
||||||
if command -v sudo >/dev/null 2>&1; then
|
RUN_AS_AUR=(runuser -u aur_builder -- bash -c)
|
||||||
RUN_AS_AUR=(sudo -u aur_builder bash -lc)
|
|
||||||
else
|
|
||||||
RUN_AS_AUR=(su - aur_builder -c)
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
|
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,22 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
echo "[arch/dependencies] Installing Arch build dependencies..."
|
echo "[arch/dependencies] Installing Arch build dependencies..."
|
||||||
|
|
||||||
pacman -Syu --noconfirm
|
pacman -Syu --noconfirm
|
||||||
|
|
||||||
|
if ! pacman-key --list-sigs &>/dev/null; then
|
||||||
|
echo "[arch/dependencies] Initializing pacman keyring..."
|
||||||
|
pacman-key --init
|
||||||
|
pacman-key --populate archlinux
|
||||||
|
fi
|
||||||
|
|
||||||
pacman -S --noconfirm --needed \
|
pacman -S --noconfirm --needed \
|
||||||
base-devel \
|
base-devel \
|
||||||
git \
|
git \
|
||||||
|
gnupg \
|
||||||
rsync \
|
rsync \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
python \
|
python \
|
||||||
|
python-pip \
|
||||||
xz
|
xz
|
||||||
|
|
||||||
pacman -Scc --noconfirm
|
pacman -Scc --noconfirm
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ echo "[arch/package] Building Arch package (makepkg --nodeps) in an isolated bui
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
|
||||||
# We must not build inside /src (mounted repo). Build in /tmp to avoid permission issues.
|
# We must not build inside /opt/src/pkgmgr (mounted repo). Build in /tmp to avoid permission issues.
|
||||||
BUILD_ROOT="/tmp/package-manager-arch-build"
|
BUILD_ROOT="/tmp/package-manager-arch-build"
|
||||||
PKG_SRC_DIR="${PROJECT_ROOT}/packaging/arch"
|
PKG_SRC_DIR="${PROJECT_ROOT}/packaging/arch"
|
||||||
PKG_BUILD_DIR="${BUILD_ROOT}/packaging/arch"
|
PKG_BUILD_DIR="${BUILD_ROOT}/packaging/arch"
|
||||||
@@ -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)"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ echo "[centos/dependencies] Installing CentOS build dependencies..."
|
|||||||
dnf -y update
|
dnf -y update
|
||||||
dnf -y install \
|
dnf -y install \
|
||||||
git \
|
git \
|
||||||
|
gnupg2 \
|
||||||
rsync \
|
rsync \
|
||||||
rpm-build \
|
rpm-build \
|
||||||
make \
|
make \
|
||||||
@@ -14,6 +15,7 @@ dnf -y install \
|
|||||||
curl-minimal \
|
curl-minimal \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
python3 \
|
python3 \
|
||||||
|
python3-pip \
|
||||||
sudo \
|
sudo \
|
||||||
xz
|
xz
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|||||||
debhelper \
|
debhelper \
|
||||||
dpkg-dev \
|
dpkg-dev \
|
||||||
git \
|
git \
|
||||||
|
gnupg \
|
||||||
rsync \
|
rsync \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
python3 \
|
python3 \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
|
python3-pip \
|
||||||
xz-utils
|
xz-utils
|
||||||
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ echo "[fedora/dependencies] Installing Fedora build dependencies..."
|
|||||||
dnf -y update
|
dnf -y update
|
||||||
dnf -y install \
|
dnf -y install \
|
||||||
git \
|
git \
|
||||||
|
gnupg2 \
|
||||||
rsync \
|
rsync \
|
||||||
rpm-build \
|
rpm-build \
|
||||||
make \
|
make \
|
||||||
@@ -14,6 +15,7 @@ dnf -y install \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
python3 \
|
python3 \
|
||||||
|
python3-pip \
|
||||||
xz
|
xz
|
||||||
|
|
||||||
dnf clean all
|
dnf clean all
|
||||||
|
|||||||
@@ -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)."
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|||||||
debhelper \
|
debhelper \
|
||||||
dpkg-dev \
|
dpkg-dev \
|
||||||
git \
|
git \
|
||||||
|
gnupg \
|
||||||
tzdata \
|
tzdata \
|
||||||
lsb-release \
|
lsb-release \
|
||||||
rsync \
|
rsync \
|
||||||
@@ -17,6 +18,7 @@ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|||||||
make \
|
make \
|
||||||
python3 \
|
python3 \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
|
python3-pip \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
xz-utils
|
xz-utils
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Ensure NIX_CONFIG has our defaults if not already set
|
|
||||||
if [[ -z "${NIX_CONFIG:-}" ]]; then
|
|
||||||
export NIX_CONFIG="experimental-features = nix-command flakes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
FLAKE_DIR="/usr/lib/package-manager"
|
FLAKE_DIR="/usr/lib/package-manager"
|
||||||
|
NIX_LIB_DIR="${FLAKE_DIR}/nix/lib"
|
||||||
|
RETRY_LIB="${NIX_LIB_DIR}/retry_403.sh"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hard requirement: retry helper must exist (fail if missing)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ ! -f "${RETRY_LIB}" ]]; then
|
||||||
|
echo "[launcher] ERROR: Required retry helper not found: ${RETRY_LIB}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Try to ensure that "nix" is on PATH (common locations + container user)
|
# Try to ensure that "nix" is on PATH (common locations + container user)
|
||||||
@@ -32,17 +37,23 @@ fi
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
if [[ -x "${FLAKE_DIR}/nix/init.sh" ]]; then
|
if [[ -x "${FLAKE_DIR}/nix/init.sh" ]]; then
|
||||||
"${FLAKE_DIR}/nix/init.sh" || true
|
"${FLAKE_DIR}/nix/init.sh"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
# Primary path: use Nix flake if available
|
echo "[launcher] ERROR: 'nix' binary not found on PATH after init." >&2
|
||||||
# ---------------------------------------------------------------------------
|
echo "[launcher] Nix is required to run pkgmgr (no Python fallback)." >&2
|
||||||
if command -v nix >/dev/null 2>&1; then
|
exit 1
|
||||||
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[pkgmgr-wrapper] ERROR: 'nix' binary not found on PATH after init."
|
# ---------------------------------------------------------------------------
|
||||||
echo "[pkgmgr-wrapper] Nix is required to run pkgmgr (no Python fallback)."
|
# Primary path: use Nix flake if available (with GitHub 403 retry)
|
||||||
exit 1
|
# ---------------------------------------------------------------------------
|
||||||
|
if declare -F run_with_github_403_retry >/dev/null; then
|
||||||
|
# shellcheck source=./scripts/nix/lib/retry_403.sh
|
||||||
|
source "${RETRY_LIB}"
|
||||||
|
exec run_with_github_403_retry nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
|
||||||
|
else
|
||||||
|
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
|
||||||
|
fi
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo ">>> Running RUFF lint on src and tests (local, no container)"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
if ! command -v ruff >/dev/null 2>&1; then
|
||||||
|
echo "ruff is not installed or not on PATH." >&2
|
||||||
|
echo "Install it with: pip install ruff" >&2
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
|
||||||
|
ruff --version
|
||||||
|
ruff check src tests
|
||||||
@@ -22,7 +22,7 @@ It is invoked during package installation (Arch/Debian/Fedora scriptlets) and ca
|
|||||||
|
|
||||||
The entry point sources small, focused modules from *scripts/nix/lib/*:
|
The entry point sources small, focused modules from *scripts/nix/lib/*:
|
||||||
|
|
||||||
- *config.sh* — configuration defaults (installer URL, retry timing)
|
- *bootstrap_config.sh* — configuration defaults (installer URL, retry timing)
|
||||||
- *detect.sh* — container detection helpers
|
- *detect.sh* — container detection helpers
|
||||||
- *path.sh* — PATH adjustments and `nix` binary resolution helpers
|
- *path.sh* — PATH adjustments and `nix` binary resolution helpers
|
||||||
- *symlinks.sh* — user/global symlink helpers for stable `nix` discovery
|
- *symlinks.sh* — user/global symlink helpers for stable `nix` discovery
|
||||||
|
|||||||
+20
-8
@@ -1,22 +1,29 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# shellcheck source=lib/config.sh
|
|
||||||
# shellcheck source=lib/detect.sh
|
|
||||||
# shellcheck source=lib/path.sh
|
|
||||||
# shellcheck source=lib/symlinks.sh
|
|
||||||
# shellcheck source=lib/users.sh
|
|
||||||
# shellcheck source=lib/install.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "${SCRIPT_DIR}/lib/config.sh"
|
# shellcheck source=./scripts/nix/lib/bootstrap_config.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/bootstrap_config.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/detect.sh
|
||||||
source "${SCRIPT_DIR}/lib/detect.sh"
|
source "${SCRIPT_DIR}/lib/detect.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/path.sh
|
||||||
source "${SCRIPT_DIR}/lib/path.sh"
|
source "${SCRIPT_DIR}/lib/path.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/symlinks.sh
|
||||||
source "${SCRIPT_DIR}/lib/symlinks.sh"
|
source "${SCRIPT_DIR}/lib/symlinks.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/users.sh
|
||||||
source "${SCRIPT_DIR}/lib/users.sh"
|
source "${SCRIPT_DIR}/lib/users.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/install.sh
|
||||||
source "${SCRIPT_DIR}/lib/install.sh"
|
source "${SCRIPT_DIR}/lib/install.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/nix_conf_file.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/nix_conf_file.sh"
|
||||||
|
|
||||||
echo "[init-nix] Starting Nix initialization..."
|
echo "[init-nix] Starting Nix initialization..."
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
@@ -26,6 +33,7 @@ main() {
|
|||||||
ensure_nix_on_path
|
ensure_nix_on_path
|
||||||
|
|
||||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||||
|
nixconf_ensure_experimental_features
|
||||||
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
|
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
|
||||||
else
|
else
|
||||||
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
|
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
|
||||||
@@ -106,6 +114,10 @@ main() {
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
ensure_nix_on_path
|
ensure_nix_on_path
|
||||||
|
|
||||||
|
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||||
|
nixconf_ensure_experimental_features
|
||||||
|
fi
|
||||||
|
|
||||||
local nix_bin_post
|
local nix_bin_post
|
||||||
nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)"
|
nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)"
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
@@ -49,11 +49,7 @@ install_nix_with_retry() {
|
|||||||
if [[ -n "$run_as" ]]; then
|
if [[ -n "$run_as" ]]; then
|
||||||
chown "$run_as:$run_as" "$installer" 2>/dev/null || true
|
chown "$run_as:$run_as" "$installer" 2>/dev/null || true
|
||||||
echo "[init-nix] Running installer as user '$run_as' ($mode_flag)..."
|
echo "[init-nix] Running installer as user '$run_as' ($mode_flag)..."
|
||||||
if command -v sudo >/dev/null 2>&1; then
|
su - "$run_as" -s /bin/bash -c "bash -lc \"sh '$installer' $mode_flag\""
|
||||||
sudo -u "$run_as" bash -lc "sh '$installer' $mode_flag"
|
|
||||||
else
|
|
||||||
su - "$run_as" -c "sh '$installer' $mode_flag"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "[init-nix] Running installer as current user ($mode_flag)..."
|
echo "[init-nix] Running installer as current user ($mode_flag)..."
|
||||||
sh "$installer" "$mode_flag"
|
sh "$installer" "$mode_flag"
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Prevent double-sourcing
|
||||||
|
if [[ -n "${PKGMGR_NIX_CONF_FILE_SH:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
PKGMGR_NIX_CONF_FILE_SH=1
|
||||||
|
|
||||||
|
nixconf_file_path() {
|
||||||
|
echo "/etc/nix/nix.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure a given nix.conf key contains required tokens (merged, no duplicates)
|
||||||
|
nixconf_ensure_features_key() {
|
||||||
|
local nix_conf="$1"
|
||||||
|
local key="$2"
|
||||||
|
shift 2
|
||||||
|
local required=("$@")
|
||||||
|
|
||||||
|
mkdir -p /etc/nix
|
||||||
|
|
||||||
|
# Create file if missing (with just the required tokens)
|
||||||
|
if [[ ! -f "${nix_conf}" ]]; then
|
||||||
|
local want="${key} = ${required[*]}"
|
||||||
|
echo "[nix-conf] Creating ${nix_conf} with: ${want}"
|
||||||
|
printf "%s\n" "${want}" >"${nix_conf}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Key exists -> merge tokens
|
||||||
|
if grep -qE "^\s*${key}\s*=" "${nix_conf}"; then
|
||||||
|
local ok=1
|
||||||
|
local t
|
||||||
|
for t in "${required[@]}"; do
|
||||||
|
if ! grep -qE "^\s*${key}\s*=.*\b${t}\b" "${nix_conf}"; then
|
||||||
|
ok=0
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$ok" -eq 1 ]]; then
|
||||||
|
echo "[nix-conf] ${key} already correct"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[nix-conf] Extending ${key} in ${nix_conf}"
|
||||||
|
|
||||||
|
local current
|
||||||
|
current="$(grep -E "^\s*${key}\s*=" "${nix_conf}" | head -n1 | cut -d= -f2-)"
|
||||||
|
current="$(echo "${current}" | xargs)" # trim
|
||||||
|
|
||||||
|
local merged=""
|
||||||
|
local token
|
||||||
|
|
||||||
|
# Start with existing tokens
|
||||||
|
for token in ${current}; do
|
||||||
|
if [[ " ${merged} " != *" ${token} "* ]]; then
|
||||||
|
merged="${merged} ${token}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Add required tokens
|
||||||
|
for token in "${required[@]}"; do
|
||||||
|
if [[ " ${merged} " != *" ${token} "* ]]; then
|
||||||
|
merged="${merged} ${token}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
merged="$(echo "${merged}" | xargs)" # trim
|
||||||
|
|
||||||
|
sed -i "s|^\s*${key}\s*=.*|${key} = ${merged}|" "${nix_conf}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Key missing -> append
|
||||||
|
local want="${key} = ${required[*]}"
|
||||||
|
echo "[nix-conf] Appending to ${nix_conf}: ${want}"
|
||||||
|
printf "\n%s\n" "${want}" >>"${nix_conf}"
|
||||||
|
}
|
||||||
|
|
||||||
|
nixconf_ensure_experimental_features() {
|
||||||
|
local nix_conf
|
||||||
|
nix_conf="$(nixconf_file_path)"
|
||||||
|
|
||||||
|
# Ensure both keys to avoid prompts and cover older/alternate expectations
|
||||||
|
nixconf_ensure_features_key "${nix_conf}" "experimental-features" "nix-command" "flakes"
|
||||||
|
nixconf_ensure_features_key "${nix_conf}" "extra-experimental-features" "nix-command" "flakes"
|
||||||
|
}
|
||||||
@@ -36,16 +36,17 @@ real_exe() {
|
|||||||
|
|
||||||
# Resolve nix binary path robustly (works across distros + Arch /usr/sbin)
|
# Resolve nix binary path robustly (works across distros + Arch /usr/sbin)
|
||||||
resolve_nix_bin() {
|
resolve_nix_bin() {
|
||||||
local nix_cmd=""
|
# IMPORTANT: prefer distro-managed locations first.
|
||||||
nix_cmd="$(command -v nix 2>/dev/null || true)"
|
# This avoids pinning /usr/local/bin/nix to a stale user-profile nix binary.
|
||||||
[[ -n "$nix_cmd" ]] && real_exe "$nix_cmd" && return 0
|
|
||||||
|
|
||||||
# IMPORTANT: prefer system locations before /usr/local to avoid self-symlink traps
|
|
||||||
[[ -x /usr/sbin/nix ]] && { echo "/usr/sbin/nix"; return 0; } # Arch package can land here
|
[[ -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 /usr/bin/nix ]] && { echo "/usr/bin/nix"; return 0; }
|
||||||
[[ -x /bin/nix ]] && { echo "/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
|
[[ -e /usr/local/bin/nix ]] && real_exe "/usr/local/bin/nix" && return 0
|
||||||
|
|
||||||
[[ -x /nix/var/nix/profiles/default/bin/nix ]] && {
|
[[ -x /nix/var/nix/profiles/default/bin/nix ]] && {
|
||||||
|
|||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -n "${PKGMGR_NIX_RETRY_403_SH:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
PKGMGR_NIX_RETRY_403_SH=1
|
||||||
|
|
||||||
|
# Retry only when we see the GitHub API rate limit 403 error during nix flake evaluation.
|
||||||
|
# Retries 7 times with delays: 10, 30, 50, 80, 130, 210, 420 seconds.
|
||||||
|
run_with_github_403_retry() {
|
||||||
|
local -a delays=(10 30 50 80 130 210 420)
|
||||||
|
local attempt=0
|
||||||
|
local max_retries="${#delays[@]}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local err tmp
|
||||||
|
tmp="$(mktemp -t nix-err.XXXXXX)"
|
||||||
|
err=0
|
||||||
|
|
||||||
|
# Run the command; capture stderr for inspection while preserving stdout.
|
||||||
|
if "$@" 2>"$tmp"; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
err=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only retry on the specific GitHub API rate limit 403 case.
|
||||||
|
if grep -qE 'HTTP error 403' "$tmp" && grep -qiE 'API rate limit exceeded|api\.github\.com' "$tmp"; then
|
||||||
|
if (( attempt >= max_retries )); then
|
||||||
|
cat "$tmp" >&2
|
||||||
|
rm -f "$tmp"
|
||||||
|
return "$err"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local sleep_s="${delays[$attempt]}"
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
|
||||||
|
echo "[nix-retry] GitHub API rate-limit (403). Retry ${attempt}/${max_retries} in ${sleep_s}s: $*" >&2
|
||||||
|
cat "$tmp" >&2
|
||||||
|
rm -f "$tmp"
|
||||||
|
sleep "$sleep_s"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Not our retry case -> fail fast with original stderr.
|
||||||
|
cat "$tmp" >&2
|
||||||
|
rm -f "$tmp"
|
||||||
|
return "$err"
|
||||||
|
done
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Nix shell mode: do not touch venv, only run install
|
# Nix shell mode: do not touch venv, only run install
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|||||||
cd "${PROJECT_ROOT}"
|
cd "${PROJECT_ROOT}"
|
||||||
|
|
||||||
VENV_DIR="${HOME}/.venvs/pkgmgr"
|
VENV_DIR="${HOME}/.venvs/pkgmgr"
|
||||||
|
# shellcheck disable=SC2016
|
||||||
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
|
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|||||||
+14
-13
@@ -2,17 +2,18 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running E2E tests: $distro"
|
echo ">>> Running E2E tests: $PKGMGR_DISTRO"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/opt/src/pkgmgr" \
|
||||||
-v "pkgmgr_nix_store_${distro}:/nix" \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
--workdir /src \
|
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||||
"pkgmgr-${distro}" \
|
--workdir /opt/src/pkgmgr \
|
||||||
|
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -40,21 +41,21 @@ docker run --rm \
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Mark the mounted repository as safe to avoid Git ownership errors.
|
# Mark the mounted repository as safe to avoid Git ownership errors.
|
||||||
# Newer Git (e.g. on Ubuntu) complains about the gitdir (/src/.git),
|
# Newer Git (e.g. on Ubuntu) complains about the gitdir (/opt/src/pkgmgr/.git),
|
||||||
# older versions about the worktree (/src). Nix turns "." into the
|
# older versions about the worktree (/opt/src/pkgmgr). Nix turns "." into the
|
||||||
# flake input "git+file:///src", which then uses Git under the hood.
|
# flake input "git+file:///opt/src/pkgmgr", which then uses Git under the hood.
|
||||||
if command -v git >/dev/null 2>&1; then
|
if command -v git >/dev/null 2>&1; then
|
||||||
# Worktree path
|
# Worktree path
|
||||||
git config --global --add safe.directory /src || true
|
git config --global --add safe.directory /opt/src/pkgmgr || true
|
||||||
# Gitdir path shown in the "dubious ownership" error
|
# Gitdir path shown in the "dubious ownership" error
|
||||||
git config --global --add safe.directory /src/.git || true
|
git config --global --add safe.directory /opt/src/pkgmgr/.git || true
|
||||||
# Ephemeral CI containers: allow all paths as a last resort
|
# Ephemeral CI containers: allow all paths as a last resort
|
||||||
git config --global --add safe.directory '*' || true
|
git config --global --add safe.directory "*" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run the E2E tests inside the Nix development shell
|
# Run the E2E tests inside the Nix development shell
|
||||||
nix develop .#default --no-write-lock-file -c \
|
nix develop .#default --no-write-lock-file -c \
|
||||||
python3 -m unittest discover \
|
python3 -m unittest discover \
|
||||||
-s /src/tests/e2e \
|
-s /opt/src/pkgmgr/tests/e2e \
|
||||||
-p "$TEST_PATTERN"
|
-p "$TEST_PATTERN"
|
||||||
'
|
'
|
||||||
@@ -1,33 +1,34 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
IMAGE="pkgmgr-${distro}"
|
IMAGE="pkgmgr-${PKGMGR_DISTRO}"
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running Nix flake-only test in ${distro} container"
|
echo ">>> Running Nix flake-only test in ${PKGMGR_DISTRO} container"
|
||||||
echo ">>> Image: ${IMAGE}"
|
echo ">>> Image: ${IMAGE}"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/opt/src/pkgmgr" \
|
||||||
-v "pkgmgr_nix_store_${distro}:/nix" \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
--workdir /src \
|
--workdir /opt/src/pkgmgr \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
|
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||||
"${IMAGE}" \
|
"${IMAGE}" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if command -v git >/dev/null 2>&1; then
|
if command -v git >/dev/null 2>&1; then
|
||||||
git config --global --add safe.directory /src || true
|
git config --global --add safe.directory /opt/src/pkgmgr || true
|
||||||
git config --global --add safe.directory /src/.git || true
|
git config --global --add safe.directory /opt/src/pkgmgr/.git || true
|
||||||
git config --global --add safe.directory "*" || true
|
git config --global --add safe.directory "*" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ">>> preflight: nix must exist in image"
|
echo ">>> preflight: nix must exist in image"
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
echo "NO_NIX"
|
echo "NO_NIX"
|
||||||
echo "ERROR: nix not found in image '\'''"${IMAGE}"''\'' (distro='"${distro}"')"
|
echo "ERROR: nix not found in image '"${IMAGE}"' (PKGMGR_DISTRO='"${PKGMGR_DISTRO}"')"
|
||||||
echo "HINT: Ensure Nix is installed during image build for this distro."
|
echo "HINT: Ensure Nix is installed during image build for this distro."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -35,14 +36,28 @@ docker run --rm \
|
|||||||
echo ">>> nix version"
|
echo ">>> nix version"
|
||||||
nix --version
|
nix --version
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Retry helper for GitHub API rate-limit (HTTP 403)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if [[ -f /opt/src/pkgmgr/scripts/nix/lib/retry_403.sh ]]; then
|
||||||
|
# shellcheck source=./scripts/nix/lib/retry_403.sh
|
||||||
|
source /opt/src/pkgmgr/scripts/nix/lib/retry_403.sh
|
||||||
|
elif [[ -f ./scripts/nix/lib/retry_403.sh ]]; then
|
||||||
|
# shellcheck source=./scripts/nix/lib/retry_403.sh
|
||||||
|
source ./scripts/nix/lib/retry_403.sh
|
||||||
|
else
|
||||||
|
echo "ERROR: retry helper not found: scripts/nix/lib/retry_403.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ">>> nix flake show"
|
echo ">>> nix flake show"
|
||||||
nix flake show . --no-write-lock-file >/dev/null
|
run_with_github_403_retry nix flake show . --no-write-lock-file >/dev/null
|
||||||
|
|
||||||
echo ">>> nix build .#default"
|
echo ">>> nix build .#default"
|
||||||
nix build .#default --no-link --no-write-lock-file
|
run_with_github_403_retry nix build .#default --no-link --no-write-lock-file
|
||||||
|
|
||||||
echo ">>> nix run .#pkgmgr -- --help"
|
echo ">>> nix run .#pkgmgr -- --help"
|
||||||
nix run .#pkgmgr -- --help --no-write-lock-file
|
run_with_github_403_retry nix run .#pkgmgr -- --help --no-write-lock-file
|
||||||
|
|
||||||
echo ">>> OK: Nix flake-only test succeeded."
|
echo ">>> OK: Nix flake-only test succeeded."
|
||||||
'
|
'
|
||||||
@@ -1,32 +1,50 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
IMAGE="pkgmgr-$distro"
|
IMAGE="pkgmgr-${PKGMGR_DISTRO}"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "------------------------------------------------------------"
|
echo "------------------------------------------------------------"
|
||||||
echo ">>> Testing VENV: $IMAGE"
|
echo ">>> Testing VENV: ${IMAGE}"
|
||||||
echo "------------------------------------------------------------"
|
echo "------------------------------------------------------------"
|
||||||
|
|
||||||
echo "[test-env-virtual] Inspect image metadata:"
|
echo "[test-env-virtual] Inspect image metadata:"
|
||||||
docker image inspect "$IMAGE" | sed -n '1,40p'
|
docker image inspect "${IMAGE}" | sed -n '1,40p'
|
||||||
|
|
||||||
echo "[test-env-virtual] Running: docker run --rm --entrypoint pkgmgr $IMAGE --help"
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Run the command and capture the output
|
# ------------------------------------------------------------
|
||||||
|
# Run VENV-based pkgmgr test inside container
|
||||||
|
# ------------------------------------------------------------
|
||||||
if OUTPUT=$(docker run --rm \
|
if OUTPUT=$(docker run --rm \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
-v "$(pwd):/opt/src/pkgmgr" \
|
||||||
-v "$(pwd):/src" \
|
-w /opt/src/pkgmgr \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||||
"$IMAGE" 2>&1); then
|
"${IMAGE}" \
|
||||||
|
bash -lc '
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "[test-env-virtual] Installing pkgmgr (distro package)..."
|
||||||
|
make install
|
||||||
|
|
||||||
|
echo "[test-env-virtual] Setting up Python venv..."
|
||||||
|
make setup-venv
|
||||||
|
|
||||||
|
echo "[test-env-virtual] Activating venv..."
|
||||||
|
. "$HOME/.venvs/pkgmgr/bin/activate"
|
||||||
|
|
||||||
|
echo "[test-env-virtual] Using pkgmgr from:"
|
||||||
|
command -v pkgmgr
|
||||||
|
pkgmgr --help
|
||||||
|
' 2>&1); then
|
||||||
|
|
||||||
echo "$OUTPUT"
|
echo "$OUTPUT"
|
||||||
echo
|
echo
|
||||||
echo "[test-env-virtual] SUCCESS: $IMAGE responded to 'pkgmgr --help'"
|
echo "[test-env-virtual] SUCCESS: venv-based pkgmgr works in ${IMAGE}"
|
||||||
|
|
||||||
else
|
else
|
||||||
echo "$OUTPUT"
|
echo "$OUTPUT"
|
||||||
echo
|
echo
|
||||||
echo "[test-env-virtual] ERROR: $IMAGE failed to run 'pkgmgr --help'"
|
echo "[test-env-virtual] ERROR: venv-based pkgmgr failed in ${IMAGE}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -2,23 +2,24 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running INTEGRATION tests in ${distro} container"
|
echo ">>> Running INTEGRATION tests in ${PKGMGR_DISTRO} container"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/opt/src/pkgmgr" \
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
--workdir /src \
|
--workdir /opt/src/pkgmgr \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
"pkgmgr-${distro}" \
|
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||||
|
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -e;
|
set -e;
|
||||||
git config --global --add safe.directory /src || true;
|
git config --global --add safe.directory /opt/src/pkgmgr || true;
|
||||||
nix develop .#default --no-write-lock-file -c \
|
nix develop .#default --no-write-lock-file -c \
|
||||||
python3 -m unittest discover \
|
python3 -m unittest discover \
|
||||||
-s tests/integration \
|
-s tests/integration \
|
||||||
-t /src \
|
-t /opt/src/pkgmgr \
|
||||||
-p "$TEST_PATTERN";
|
-p "$TEST_PATTERN";
|
||||||
'
|
'
|
||||||
@@ -2,23 +2,24 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running UNIT tests in ${distro} container"
|
echo ">>> Running UNIT tests in ${PKGMGR_DISTRO} container"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/opt/src/pkgmgr" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
--workdir /src \
|
--workdir /opt/src/pkgmgr \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
"pkgmgr-${distro}" \
|
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||||
|
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -e;
|
set -e;
|
||||||
git config --global --add safe.directory /src || true;
|
git config --global --add safe.directory /opt/src/pkgmgr || true;
|
||||||
nix develop .#default --no-write-lock-file -c \
|
nix develop .#default --no-write-lock-file -c \
|
||||||
python3 -m unittest discover \
|
python3 -m unittest discover \
|
||||||
-s tests/unit \
|
-s tests/unit \
|
||||||
-t /src \
|
-t /opt/src/pkgmgr \
|
||||||
-p "$TEST_PATTERN";
|
-p "$TEST_PATTERN";
|
||||||
'
|
'
|
||||||
+10
-2
@@ -19,12 +19,20 @@ fi
|
|||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Remove auto-activation lines from shell RC files
|
# Remove auto-activation lines from shell RC files
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
RC_PATTERN='\.venvs\/pkgmgr\/bin\/activate"; if \[ -n "\$${PS1:-}" \]; then echo "Global Python virtual environment '\''~\/\.venvs\/pkgmgr'\'' activated."; fi; fi'
|
# Matches:
|
||||||
|
# ~/.venvs/pkgmgr/bin/activate
|
||||||
|
# ./.venvs/pkgmgr/bin/activate
|
||||||
|
RC_PATTERN='(\./)?\.venvs/pkgmgr/bin/activate'
|
||||||
|
|
||||||
echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..."
|
echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..."
|
||||||
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
||||||
if [[ -f "$rc" ]]; then
|
if [[ -f "$rc" ]]; then
|
||||||
sed -i "/$RC_PATTERN/d" "$rc"
|
# Remove activation lines (functional)
|
||||||
|
sed -E -i "/$RC_PATTERN/d" "$rc"
|
||||||
|
|
||||||
|
# Remove leftover echo / cosmetic lines referencing pkgmgr venv
|
||||||
|
sed -i '/\.venvs\/pkgmgr/d' "$rc"
|
||||||
|
|
||||||
echo "[uninstall] Cleaned $rc"
|
echo "[uninstall] Cleaned $rc"
|
||||||
else
|
else
|
||||||
echo "[uninstall] File not found: $rc (skipped)"
|
echo "[uninstall] File not found: $rc (skipped)"
|
||||||
|
|||||||
+8
-11
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Top-level pkgmgr package.
|
Top-level pkgmgr package.
|
||||||
|
|
||||||
@@ -25,12 +22,12 @@ __all__ = ["cli"]
|
|||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> Any:
|
def __getattr__(name: str) -> Any:
|
||||||
"""
|
"""
|
||||||
Lazily expose ``pkgmgr.cli`` as attribute on the top-level package.
|
Lazily expose ``pkgmgr.cli`` as attribute on the top-level package.
|
||||||
|
|
||||||
This keeps ``import pkgmgr`` lightweight while still allowing
|
This keeps ``import pkgmgr`` lightweight while still allowing
|
||||||
``from pkgmgr import cli`` in tests and entry points.
|
``from pkgmgr import cli`` in tests and entry points.
|
||||||
"""
|
"""
|
||||||
if name == "cli":
|
if name == "cli":
|
||||||
return import_module("pkgmgr.cli")
|
return import_module("pkgmgr.cli")
|
||||||
raise AttributeError(f"module 'pkgmgr' has no attribute {name!r}")
|
raise AttributeError(f"module 'pkgmgr' has no attribute {name!r}")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# expose subpackages for patch() / resolve_name() friendliness
|
||||||
|
from . import release as release
|
||||||
|
|
||||||
|
__all__ = ["release"]
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Archive fully-checked Markdown files into a README index, then delete them.
|
||||||
|
|
||||||
|
The archive action walks a directory for numbered ``NNN-topic.md`` files
|
||||||
|
(default pattern ``^\\d{3}-[^/]+\\.md$``), promotes every file with zero
|
||||||
|
unchecked ``- [ ]`` task-list markers into a ``## Archive`` index inside
|
||||||
|
the directory README, and deletes the per-file source. Useful for
|
||||||
|
keeping ``docs/requirements/`` (or any other task-tracked spec folder)
|
||||||
|
short and focused on open work.
|
||||||
|
|
||||||
|
The module was extracted from
|
||||||
|
``cli/contributing/requirements/archive`` in infinito-nexus-core so
|
||||||
|
every kpmx-managed repository can rely on the same archival convention
|
||||||
|
without copy-pasting the helpers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .discovery import iter_archivable_files
|
||||||
|
from .inspect import count_unchecked_items, extract_h1
|
||||||
|
from .readme import existing_archive_entries, merge_archive_section
|
||||||
|
from .workflow import ArchivePlan, run_archive
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ArchivePlan",
|
||||||
|
"count_unchecked_items",
|
||||||
|
"existing_archive_entries",
|
||||||
|
"extract_h1",
|
||||||
|
"iter_archivable_files",
|
||||||
|
"merge_archive_section",
|
||||||
|
"run_archive",
|
||||||
|
]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Locate archivable Markdown files under a target directory."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_FILENAME_PATTERN = re.compile(r"^\d{3}-[^/]+\.md$")
|
||||||
|
TEMPLATE_FILENAME = "000-template.md"
|
||||||
|
|
||||||
|
|
||||||
|
def iter_archivable_files(
|
||||||
|
directory: Path,
|
||||||
|
*,
|
||||||
|
include_template: bool = False,
|
||||||
|
pattern: re.Pattern[str] = DEFAULT_FILENAME_PATTERN,
|
||||||
|
template_filename: str = TEMPLATE_FILENAME,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Return all files in *directory* whose name matches *pattern*, sorted.
|
||||||
|
|
||||||
|
``000-template.md`` (or whatever *template_filename* matches) is
|
||||||
|
excluded unless *include_template* is true. The check is filename
|
||||||
|
based; nested directories are not traversed.
|
||||||
|
"""
|
||||||
|
if not directory.is_dir():
|
||||||
|
return []
|
||||||
|
files: list[Path] = []
|
||||||
|
for path in sorted(directory.iterdir()):
|
||||||
|
if not path.is_file() or not pattern.match(path.name):
|
||||||
|
continue
|
||||||
|
if not include_template and path.name == template_filename:
|
||||||
|
continue
|
||||||
|
files.append(path)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def filter_archivable_files(
|
||||||
|
paths: Iterable[Path],
|
||||||
|
*,
|
||||||
|
include_template: bool = False,
|
||||||
|
pattern: re.Pattern[str] = DEFAULT_FILENAME_PATTERN,
|
||||||
|
template_filename: str = TEMPLATE_FILENAME,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Same predicate as :func:`iter_archivable_files`, applied to an iterable."""
|
||||||
|
result: list[Path] = []
|
||||||
|
for path in paths:
|
||||||
|
if not path.is_file() or not pattern.match(path.name):
|
||||||
|
continue
|
||||||
|
if not include_template and path.name == template_filename:
|
||||||
|
continue
|
||||||
|
result.append(path)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Parse a single Markdown file: H1 heading and task-list completeness."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
H1_RE = re.compile(r"^#\s+(?P<title>\S.*?)\s*$")
|
||||||
|
UNCHECKED_TASK_RE = re.compile(r"^\s*[-*+]\s+\[\s\]\s")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_h1(path: Path) -> str | None:
|
||||||
|
"""Return the first H1 title in *path* or ``None`` if there is none."""
|
||||||
|
try:
|
||||||
|
with path.open(encoding="utf-8") as fh:
|
||||||
|
for line in fh:
|
||||||
|
match = H1_RE.match(line.rstrip("\n"))
|
||||||
|
if match:
|
||||||
|
return match.group("title")
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def count_unchecked_items(path: Path) -> int:
|
||||||
|
"""Return the number of ``- [ ]`` task-list markers anywhere in *path*.
|
||||||
|
|
||||||
|
A non-zero count means the file is not yet fully complete and MUST
|
||||||
|
NOT be archived.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with path.open(encoding="utf-8") as fh:
|
||||||
|
return sum(1 for line in fh if UNCHECKED_TASK_RE.match(line))
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""Read and update the ``## Archive`` section of a directory README."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
ARCHIVE_HEADING = "## Archive"
|
||||||
|
LIST_ITEM_RE = re.compile(r"^\s*-\s+(?P<body>\S.*)$")
|
||||||
|
|
||||||
|
|
||||||
|
def existing_archive_entries(readme_text: str) -> set[str]:
|
||||||
|
"""Return the deduplicated set of list-item bodies under ``## Archive``."""
|
||||||
|
lines = readme_text.splitlines()
|
||||||
|
in_archive = False
|
||||||
|
entries: set[str] = set()
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.rstrip()
|
||||||
|
if stripped == ARCHIVE_HEADING:
|
||||||
|
in_archive = True
|
||||||
|
continue
|
||||||
|
if in_archive and stripped.startswith("## "):
|
||||||
|
break
|
||||||
|
if not in_archive:
|
||||||
|
continue
|
||||||
|
match = LIST_ITEM_RE.match(stripped)
|
||||||
|
if match:
|
||||||
|
entries.add(match.group("body").strip())
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def merge_archive_section(readme_text: str, new_entries: list[str]) -> str:
|
||||||
|
"""Return ``readme_text`` with *new_entries* appended under ``## Archive``.
|
||||||
|
|
||||||
|
Existing entries are preserved verbatim. If the section is missing it
|
||||||
|
is created at the end of the document.
|
||||||
|
"""
|
||||||
|
if not new_entries:
|
||||||
|
return readme_text
|
||||||
|
|
||||||
|
lines = readme_text.splitlines()
|
||||||
|
archive_index = next(
|
||||||
|
(i for i, line in enumerate(lines) if line.rstrip() == ARCHIVE_HEADING),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if archive_index is None:
|
||||||
|
suffix = [""] if (lines and lines[-1] != "") else []
|
||||||
|
suffix.append(ARCHIVE_HEADING)
|
||||||
|
suffix.append("")
|
||||||
|
suffix.extend(f"- {entry}" for entry in new_entries)
|
||||||
|
merged = lines + suffix
|
||||||
|
return "\n".join(merged) + "\n"
|
||||||
|
|
||||||
|
section_end = next(
|
||||||
|
(i for i in range(archive_index + 1, len(lines)) if lines[i].startswith("## ")),
|
||||||
|
len(lines),
|
||||||
|
)
|
||||||
|
|
||||||
|
body_start = archive_index + 1
|
||||||
|
while body_start < section_end and not lines[body_start].strip():
|
||||||
|
body_start += 1
|
||||||
|
|
||||||
|
last_item = body_start - 1
|
||||||
|
for i in range(body_start, section_end):
|
||||||
|
if LIST_ITEM_RE.match(lines[i]):
|
||||||
|
last_item = i
|
||||||
|
|
||||||
|
insertion_point = (last_item + 1) if last_item >= body_start else body_start
|
||||||
|
if insertion_point == body_start and body_start == archive_index + 1:
|
||||||
|
new_block = ["", *[f"- {entry}" for entry in new_entries]]
|
||||||
|
else:
|
||||||
|
new_block = [f"- {entry}" for entry in new_entries]
|
||||||
|
|
||||||
|
merged = lines[:insertion_point] + new_block + lines[insertion_point:]
|
||||||
|
trailing = "\n" if readme_text.endswith("\n") else ""
|
||||||
|
return "\n".join(merged) + trailing
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Orchestrator for archiving fully-checked Markdown files."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .discovery import iter_archivable_files
|
||||||
|
from .inspect import count_unchecked_items, extract_h1
|
||||||
|
from .readme import existing_archive_entries, merge_archive_section
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ArchivePlan:
|
||||||
|
"""Outcome of an archive analysis run.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
archived: ``(source_path, title)`` for every file that was (or
|
||||||
|
would be) archived. Order matches the original directory
|
||||||
|
listing.
|
||||||
|
skipped_incomplete: ``(source_path, unchecked_count)`` for files
|
||||||
|
that still hold ``- [ ]`` markers.
|
||||||
|
skipped_without_h1: files that had no H1 heading to use as title.
|
||||||
|
new_entries: titles that will be appended to the README index.
|
||||||
|
existing_entries: titles already present in the README index.
|
||||||
|
"""
|
||||||
|
|
||||||
|
archived: list[tuple[Path, str]]
|
||||||
|
skipped_incomplete: list[tuple[Path, int]]
|
||||||
|
skipped_without_h1: list[Path]
|
||||||
|
new_entries: list[str]
|
||||||
|
existing_entries: set[str]
|
||||||
|
|
||||||
|
|
||||||
|
def _bucket_files(
|
||||||
|
files: list[Path],
|
||||||
|
) -> tuple[
|
||||||
|
list[tuple[Path, str]],
|
||||||
|
list[tuple[Path, int]],
|
||||||
|
list[Path],
|
||||||
|
]:
|
||||||
|
plan: list[tuple[Path, str]] = []
|
||||||
|
skipped_incomplete: list[tuple[Path, int]] = []
|
||||||
|
skipped_without_h1: list[Path] = []
|
||||||
|
for path in files:
|
||||||
|
unchecked = count_unchecked_items(path)
|
||||||
|
if unchecked > 0:
|
||||||
|
skipped_incomplete.append((path, unchecked))
|
||||||
|
continue
|
||||||
|
title = extract_h1(path)
|
||||||
|
if title is None:
|
||||||
|
skipped_without_h1.append(path)
|
||||||
|
continue
|
||||||
|
plan.append((path, title))
|
||||||
|
return plan, skipped_incomplete, skipped_without_h1
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_titles(
|
||||||
|
plan: list[tuple[Path, str]], already_archived: set[str]
|
||||||
|
) -> list[str]:
|
||||||
|
new_entries: list[str] = []
|
||||||
|
for _path, title in plan:
|
||||||
|
if title in already_archived or title in new_entries:
|
||||||
|
continue
|
||||||
|
new_entries.append(title)
|
||||||
|
return new_entries
|
||||||
|
|
||||||
|
|
||||||
|
def run_archive(
|
||||||
|
directory: Path,
|
||||||
|
readme_path: Path,
|
||||||
|
*,
|
||||||
|
dry_run: bool = False,
|
||||||
|
include_template: bool = False,
|
||||||
|
) -> ArchivePlan:
|
||||||
|
"""Walk *directory* and archive every fully-checked file into *readme_path*.
|
||||||
|
|
||||||
|
Returns an :class:`ArchivePlan` describing the outcome. When
|
||||||
|
``dry_run`` is true no files are deleted and the README is not
|
||||||
|
rewritten — the plan still reflects what *would* happen.
|
||||||
|
|
||||||
|
Raises ``FileNotFoundError`` if *directory* or *readme_path* does
|
||||||
|
not exist.
|
||||||
|
"""
|
||||||
|
if not directory.is_dir():
|
||||||
|
raise FileNotFoundError(f"Archive directory not found: {directory}")
|
||||||
|
if not readme_path.is_file():
|
||||||
|
raise FileNotFoundError(f"README not found: {readme_path}")
|
||||||
|
|
||||||
|
files = iter_archivable_files(directory, include_template=include_template)
|
||||||
|
readme_text = readme_path.read_text(encoding="utf-8")
|
||||||
|
already_archived = existing_archive_entries(readme_text)
|
||||||
|
|
||||||
|
archived, skipped_incomplete, skipped_without_h1 = _bucket_files(files)
|
||||||
|
new_entries = _dedupe_titles(archived, already_archived)
|
||||||
|
|
||||||
|
if not dry_run and new_entries:
|
||||||
|
merged_text = merge_archive_section(readme_text, new_entries)
|
||||||
|
if merged_text != readme_text:
|
||||||
|
readme_path.write_text(merged_text, encoding="utf-8")
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
for path, _title in archived:
|
||||||
|
with contextlib.suppress(FileNotFoundError):
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
return ArchivePlan(
|
||||||
|
archived=archived,
|
||||||
|
skipped_incomplete=skipped_incomplete,
|
||||||
|
skipped_without_h1=skipped_without_h1,
|
||||||
|
new_entries=new_entries,
|
||||||
|
existing_entries=already_archived,
|
||||||
|
)
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
"""
|
||||||
Public API for branch actions.
|
Public API for branch actions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .open_branch import open_branch
|
|
||||||
from .close_branch import close_branch
|
from .close_branch import close_branch
|
||||||
from .drop_branch import drop_branch
|
from .drop_branch import drop_branch
|
||||||
|
from .gc import branch_purge, cleanup
|
||||||
|
from .open_branch import open_branch
|
||||||
|
from .tree import branch_tree, refresh_tree_entry
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"open_branch",
|
"branch_purge",
|
||||||
|
"branch_tree",
|
||||||
|
"cleanup",
|
||||||
"close_branch",
|
"close_branch",
|
||||||
"drop_branch",
|
"drop_branch",
|
||||||
|
"open_branch",
|
||||||
|
"refresh_tree_entry",
|
||||||
]
|
]
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
|
||||||
from pkgmgr.core.git import run_git, GitError, get_current_branch
|
from pkgmgr.core.git.commands import (
|
||||||
from .utils import _resolve_base_branch
|
GitDeleteRemoteBranchError,
|
||||||
|
checkout,
|
||||||
|
delete_local_branch,
|
||||||
|
delete_remote_branch,
|
||||||
|
fetch,
|
||||||
|
merge_no_ff,
|
||||||
|
pull,
|
||||||
|
push,
|
||||||
|
)
|
||||||
|
from pkgmgr.core.git.errors import GitRunError
|
||||||
|
from pkgmgr.core.git.queries import get_current_branch, resolve_base_branch
|
||||||
|
|
||||||
|
|
||||||
def close_branch(
|
def close_branch(
|
||||||
name: Optional[str],
|
name: str | None,
|
||||||
base_branch: str = "main",
|
base_branch: str = "main",
|
||||||
fallback_base: str = "master",
|
fallback_base: str = "master",
|
||||||
cwd: str = ".",
|
cwd: str = ".",
|
||||||
@@ -14,18 +24,17 @@ def close_branch(
|
|||||||
"""
|
"""
|
||||||
Merge a feature branch into the base branch and delete it afterwards.
|
Merge a feature branch into the base branch and delete it afterwards.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Determine branch name
|
# Determine branch name
|
||||||
if not name:
|
if not name:
|
||||||
try:
|
try:
|
||||||
name = get_current_branch(cwd=cwd)
|
name = get_current_branch(cwd=cwd)
|
||||||
except GitError as exc:
|
except GitRunError as exc:
|
||||||
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
|
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
raise RuntimeError("Branch name must not be empty.")
|
raise RuntimeError("Branch name must not be empty.")
|
||||||
|
|
||||||
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
target_base = resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
||||||
|
|
||||||
if name == target_base:
|
if name == target_base:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -35,65 +44,31 @@ def close_branch(
|
|||||||
|
|
||||||
# Confirmation
|
# Confirmation
|
||||||
if not force:
|
if not force:
|
||||||
answer = input(
|
answer = (
|
||||||
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? (y/N): "
|
input(
|
||||||
).strip().lower()
|
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? (y/N): "
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
if answer != "y":
|
if answer != "y":
|
||||||
print("Aborted closing branch.")
|
print("Aborted closing branch.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fetch
|
# Execute workflow (commands raise specific GitRunError subclasses)
|
||||||
try:
|
fetch("origin", cwd=cwd)
|
||||||
run_git(["fetch", "origin"], cwd=cwd)
|
checkout(target_base, cwd=cwd)
|
||||||
except GitError as exc:
|
pull("origin", target_base, cwd=cwd)
|
||||||
raise RuntimeError(
|
merge_no_ff(name, cwd=cwd)
|
||||||
f"Failed to fetch from origin before closing branch {name!r}: {exc}"
|
push("origin", target_base, cwd=cwd)
|
||||||
) from exc
|
|
||||||
|
|
||||||
# Checkout base
|
# Delete local branch (safe delete by default)
|
||||||
try:
|
delete_local_branch(name, cwd=cwd, force=False)
|
||||||
run_git(["checkout", target_base], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to checkout base branch {target_base!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# Pull latest
|
# Delete remote branch (special-case error message)
|
||||||
try:
|
try:
|
||||||
run_git(["pull", "origin", target_base], cwd=cwd)
|
delete_remote_branch("origin", name, cwd=cwd)
|
||||||
except GitError as exc:
|
except GitDeleteRemoteBranchError as exc:
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to pull latest changes for base branch {target_base!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# Merge
|
|
||||||
try:
|
|
||||||
run_git(["merge", "--no-ff", name], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to merge branch {name!r} into {target_base!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# Push result
|
|
||||||
try:
|
|
||||||
run_git(["push", "origin", target_base], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to push base branch {target_base!r} after merge: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# Delete local
|
|
||||||
try:
|
|
||||||
run_git(["branch", "-d", name], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to delete local branch {name!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# Delete remote
|
|
||||||
try:
|
|
||||||
run_git(["push", "origin", "--delete", name], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Branch {name!r} deleted locally, but remote deletion failed: {exc}"
|
f"Branch {name!r} deleted locally, but remote deletion failed: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
|
||||||
from pkgmgr.core.git import run_git, GitError, get_current_branch
|
from pkgmgr.core.git.commands import (
|
||||||
from .utils import _resolve_base_branch
|
GitDeleteRemoteBranchError,
|
||||||
|
delete_local_branch,
|
||||||
|
delete_remote_branch,
|
||||||
|
)
|
||||||
|
from pkgmgr.core.git.errors import GitRunError
|
||||||
|
from pkgmgr.core.git.queries import get_current_branch, resolve_base_branch
|
||||||
|
|
||||||
|
|
||||||
def drop_branch(
|
def drop_branch(
|
||||||
name: Optional[str],
|
name: str | None,
|
||||||
base_branch: str = "main",
|
base_branch: str = "main",
|
||||||
fallback_base: str = "master",
|
fallback_base: str = "master",
|
||||||
cwd: str = ".",
|
cwd: str = ".",
|
||||||
@@ -14,17 +19,16 @@ def drop_branch(
|
|||||||
"""
|
"""
|
||||||
Delete a branch locally and remotely without merging.
|
Delete a branch locally and remotely without merging.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
try:
|
try:
|
||||||
name = get_current_branch(cwd=cwd)
|
name = get_current_branch(cwd=cwd)
|
||||||
except GitError as exc:
|
except GitRunError as exc:
|
||||||
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
|
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
raise RuntimeError("Branch name must not be empty.")
|
raise RuntimeError("Branch name must not be empty.")
|
||||||
|
|
||||||
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
target_base = resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
||||||
|
|
||||||
if name == target_base:
|
if name == target_base:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -33,23 +37,23 @@ def drop_branch(
|
|||||||
|
|
||||||
# Confirmation
|
# Confirmation
|
||||||
if not force:
|
if not force:
|
||||||
answer = input(
|
answer = (
|
||||||
f"Delete branch '{name}' locally and on origin? This is destructive! (y/N): "
|
input(
|
||||||
).strip().lower()
|
f"Delete branch '{name}' locally and on origin? This is destructive! (y/N): "
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
if answer != "y":
|
if answer != "y":
|
||||||
print("Aborted dropping branch.")
|
print("Aborted dropping branch.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Local delete
|
delete_local_branch(name, cwd=cwd, force=False)
|
||||||
try:
|
|
||||||
run_git(["branch", "-d", name], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(f"Failed to delete local branch {name!r}: {exc}") from exc
|
|
||||||
|
|
||||||
# Remote delete
|
# Remote delete (special-case message)
|
||||||
try:
|
try:
|
||||||
run_git(["push", "origin", "--delete", name], cwd=cwd)
|
delete_remote_branch("origin", name, cwd=cwd)
|
||||||
except GitError as exc:
|
except GitDeleteRemoteBranchError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
|
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,116 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from pkgmgr.actions.mirror.url_utils import parse_repo_from_git_url
|
||||||
|
from pkgmgr.core.credentials.resolver import ResolutionOptions, TokenResolver
|
||||||
|
from pkgmgr.core.credentials.types import CredentialError
|
||||||
|
from pkgmgr.core.remote_provisioning.http.client import HttpClient
|
||||||
|
from pkgmgr.core.remote_provisioning.http.errors import HttpError
|
||||||
|
|
||||||
|
GITHUB_HOST = "github.com"
|
||||||
|
GITHUB_API = "https://api.github.com"
|
||||||
|
PAGE_SIZE = 100
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ForgeFacts:
|
||||||
|
"""
|
||||||
|
Pull-request and protection facts of every GitHub remote of one repository.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
merged_heads: head SHA of a merged PR -> PR number.
|
||||||
|
open_heads: head branch name of an open PR -> PR number.
|
||||||
|
protected: branch names protected on any of the remotes.
|
||||||
|
unavailable: why the facts could not be fetched, or None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
merged_heads: dict[str, int] = field(default_factory=dict)
|
||||||
|
open_heads: dict[str, int] = field(default_factory=dict)
|
||||||
|
protected: set[str] = field(default_factory=set)
|
||||||
|
unavailable: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def github_slugs(remote_urls: dict[str, str]) -> list[tuple[str, str]]:
|
||||||
|
slugs: list[tuple[str, str]] = []
|
||||||
|
for url in remote_urls.values():
|
||||||
|
host, owner, name = parse_repo_from_git_url(url)
|
||||||
|
if (
|
||||||
|
host.lower() == GITHUB_HOST
|
||||||
|
and owner
|
||||||
|
and name
|
||||||
|
and (owner, name) not in slugs
|
||||||
|
):
|
||||||
|
slugs.append((owner, name))
|
||||||
|
return slugs
|
||||||
|
|
||||||
|
|
||||||
|
def _pages(http: HttpClient, headers: dict[str, str], url: str) -> list[dict]:
|
||||||
|
items: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
resp = http.request_json(
|
||||||
|
"GET", f"{url}&per_page={PAGE_SIZE}&page={page}", headers=headers
|
||||||
|
)
|
||||||
|
batch = json.loads(resp.text or "[]")
|
||||||
|
items.extend(batch)
|
||||||
|
if len(batch) < PAGE_SIZE:
|
||||||
|
return items
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_forge_facts(remote_urls: dict[str, str]) -> ForgeFacts:
|
||||||
|
facts = ForgeFacts()
|
||||||
|
slugs = github_slugs(remote_urls)
|
||||||
|
if not slugs:
|
||||||
|
return facts
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = (
|
||||||
|
TokenResolver()
|
||||||
|
.get_token(
|
||||||
|
"github",
|
||||||
|
GITHUB_HOST,
|
||||||
|
options=ResolutionOptions(interactive=False, allow_prompt=False),
|
||||||
|
)
|
||||||
|
.token
|
||||||
|
)
|
||||||
|
except CredentialError as exc:
|
||||||
|
facts.unavailable = f"no GitHub token ({exc})"
|
||||||
|
return facts
|
||||||
|
return facts_with_token(slugs, token)
|
||||||
|
|
||||||
|
|
||||||
|
def facts_with_token(slugs: list[tuple[str, str]], token: str) -> ForgeFacts:
|
||||||
|
"""
|
||||||
|
Fetch the pull-request and protection facts of GitHub repositories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slugs: (owner, name) of each repository.
|
||||||
|
token: the GitHub token sent as bearer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The facts, or facts whose unavailable names the failing repository.
|
||||||
|
"""
|
||||||
|
facts = ForgeFacts()
|
||||||
|
http = HttpClient(timeout_s=30)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"User-Agent": "pkgmgr",
|
||||||
|
}
|
||||||
|
for owner, name in slugs:
|
||||||
|
base = f"{GITHUB_API}/repos/{owner}/{name}"
|
||||||
|
try:
|
||||||
|
for pr in _pages(http, headers, f"{base}/pulls?state=all"):
|
||||||
|
if pr["merged_at"]:
|
||||||
|
facts.merged_heads[pr["head"]["sha"]] = pr["number"]
|
||||||
|
elif pr["state"] == "open":
|
||||||
|
facts.open_heads[pr["head"]["ref"]] = pr["number"]
|
||||||
|
for branch in _pages(http, headers, f"{base}/branches?protected=true"):
|
||||||
|
facts.protected.add(branch["name"])
|
||||||
|
except (HttpError, ValueError, KeyError, TypeError) as exc:
|
||||||
|
facts.unavailable = f"GitHub API failed for {owner}/{name}: {exc}"
|
||||||
|
return facts
|
||||||
|
return facts
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
DIR_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
||||||
|
|
||||||
|
|
||||||
|
def canonical(path: str) -> bool:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
path: a path as git or a plan names it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True when path is absolute and already its own realpath: no symbolic
|
||||||
|
link and no `..` component that the kernel could resolve elsewhere.
|
||||||
|
"""
|
||||||
|
return os.path.isabs(path) and os.path.realpath(path) == path
|
||||||
|
|
||||||
|
|
||||||
|
def remove_tree(path: str) -> None:
|
||||||
|
"""
|
||||||
|
Delete a directory tree without following a symbolic link anywhere on
|
||||||
|
the way to it, even one swapped in after the caller checked the path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: absolute, canonical directory to delete.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OSError: a component of path is a symbolic link or missing, or the
|
||||||
|
deletion failed.
|
||||||
|
"""
|
||||||
|
parent, name = os.path.split(path)
|
||||||
|
fd = os.open(os.sep, os.O_RDONLY | os.O_DIRECTORY)
|
||||||
|
try:
|
||||||
|
for component in [c for c in parent.split(os.sep) if c]:
|
||||||
|
child = os.open(component, DIR_FLAGS, dir_fd=fd)
|
||||||
|
os.close(fd)
|
||||||
|
fd = child
|
||||||
|
# Trip-wire: rmtree(dir_fd=) needs Python 3.11; the fd link pins the
|
||||||
|
# opened parent on every version, and rmtree refuses a symlinked name.
|
||||||
|
shutil.rmtree(f"/proc/self/fd/{fd}/{name}")
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
|
||||||
from pkgmgr.core.git import run_git, GitError
|
from pkgmgr.core.git.commands import (
|
||||||
from .utils import _resolve_base_branch
|
checkout,
|
||||||
|
create_branch,
|
||||||
|
fetch,
|
||||||
|
pull,
|
||||||
|
push_upstream,
|
||||||
|
)
|
||||||
|
from pkgmgr.core.git.queries import resolve_base_branch
|
||||||
|
|
||||||
|
|
||||||
def open_branch(
|
def open_branch(
|
||||||
name: Optional[str],
|
name: str | None,
|
||||||
base_branch: str = "main",
|
base_branch: str = "main",
|
||||||
fallback_base: str = "master",
|
fallback_base: str = "master",
|
||||||
cwd: str = ".",
|
cwd: str = ".",
|
||||||
@@ -13,7 +19,6 @@ def open_branch(
|
|||||||
"""
|
"""
|
||||||
Create and push a new feature branch on top of a base branch.
|
Create and push a new feature branch on top of a base branch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Request name interactively if not provided
|
# Request name interactively if not provided
|
||||||
if not name:
|
if not name:
|
||||||
name = input("Enter new branch name: ").strip()
|
name = input("Enter new branch name: ").strip()
|
||||||
@@ -21,44 +26,13 @@ def open_branch(
|
|||||||
if not name:
|
if not name:
|
||||||
raise RuntimeError("Branch name must not be empty.")
|
raise RuntimeError("Branch name must not be empty.")
|
||||||
|
|
||||||
resolved_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
resolved_base = resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
||||||
|
|
||||||
# 1) Fetch from origin
|
# Workflow (commands raise specific GitBaseError subclasses)
|
||||||
try:
|
fetch("origin", cwd=cwd)
|
||||||
run_git(["fetch", "origin"], cwd=cwd)
|
checkout(resolved_base, cwd=cwd)
|
||||||
except GitError as exc:
|
pull("origin", resolved_base, cwd=cwd)
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to fetch from origin before creating branch {name!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# 2) Checkout base branch
|
# Create new branch from resolved base and push it with upstream tracking
|
||||||
try:
|
create_branch(name, resolved_base, cwd=cwd)
|
||||||
run_git(["checkout", resolved_base], cwd=cwd)
|
push_upstream("origin", name, cwd=cwd)
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to checkout base branch {resolved_base!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# 3) Pull latest changes
|
|
||||||
try:
|
|
||||||
run_git(["pull", "origin", resolved_base], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to pull latest changes for base branch {resolved_base!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# 4) Create new branch
|
|
||||||
try:
|
|
||||||
run_git(["checkout", "-b", name], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to create new branch {name!r} from base {resolved_base!r}: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# 5) Push new branch
|
|
||||||
try:
|
|
||||||
run_git(["push", "-u", "origin", name], cwd=cwd)
|
|
||||||
except GitError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to push new branch {name!r} to origin: {exc}"
|
|
||||||
) from exc
|
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from pkgmgr.actions.worktree import flow
|
||||||
|
from pkgmgr.core.credentials.store_keys import build_keyring_key
|
||||||
|
from pkgmgr.core.git.errors import GitBaseError
|
||||||
|
|
||||||
|
from . import gc
|
||||||
|
from .gc_forge import GITHUB_HOST, ForgeFacts, facts_with_token, github_slugs
|
||||||
|
|
||||||
|
WORKTREES_ROOT = os.path.join(
|
||||||
|
os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"),
|
||||||
|
"worktrees",
|
||||||
|
)
|
||||||
|
WORKTREE_DEPTH = 6
|
||||||
|
CACHE_TTL = 600
|
||||||
|
SLUG = re.compile(r"^(?!\.\.?$)[\w.-]+$")
|
||||||
|
|
||||||
|
|
||||||
|
def now() -> float:
|
||||||
|
return time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def _iso(stamp: float) -> str:
|
||||||
|
return datetime.fromtimestamp(stamp, timezone.utc).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
def tree_file() -> str:
|
||||||
|
state = os.environ.get("XDG_STATE_HOME") or os.path.expanduser("~/.local/state")
|
||||||
|
return os.environ.get("PKGMGR_TREE_FILE") or os.path.join(
|
||||||
|
state, "pkgmgr", "tree", "branch-tree.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_path(owner: str, name: str) -> str:
|
||||||
|
base = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache")
|
||||||
|
return os.path.join(base, "pkgmgr", "forge", owner, f"{name}.json")
|
||||||
|
|
||||||
|
|
||||||
|
def secret_service_token() -> tuple[str | None, str | None]:
|
||||||
|
"""
|
||||||
|
Read the GitHub token from the Secret Service without unlocking it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(token, None), or (None, why no token is available).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import secretstorage
|
||||||
|
except ImportError:
|
||||||
|
return None, "secretstorage not installed"
|
||||||
|
key = build_keyring_key("github", GITHUB_HOST, None)
|
||||||
|
try:
|
||||||
|
connection = secretstorage.dbus_init()
|
||||||
|
try:
|
||||||
|
items = list(
|
||||||
|
secretstorage.search_items(
|
||||||
|
connection, {"service": key.service, "username": key.username}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
unlocked = [item for item in items if not item.is_locked()]
|
||||||
|
if not items:
|
||||||
|
return None, "no GitHub token in the Secret Service"
|
||||||
|
if not unlocked:
|
||||||
|
return None, "Secret Service locked"
|
||||||
|
return unlocked[0].get_secret().decode(), None
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
except (secretstorage.exceptions.SecretStorageException, OSError) as exc:
|
||||||
|
return None, f"Secret Service unavailable: {exc}"
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _locked(path: str) -> Iterator[None]:
|
||||||
|
folder, name = os.path.split(os.path.abspath(path))
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
with open(os.path.join(folder, f".{name}.lock"), "w", encoding="utf-8") as lock:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def _replace(path: str, text: str) -> None:
|
||||||
|
folder, name = os.path.split(os.path.abspath(path))
|
||||||
|
fd, tmp = tempfile.mkstemp(dir=folder, prefix=f".{name}.")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
fh.flush()
|
||||||
|
os.fsync(fh.fileno())
|
||||||
|
os.replace(tmp, path)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp):
|
||||||
|
os.unlink(tmp)
|
||||||
|
|
||||||
|
|
||||||
|
def _write(path: str, text: str) -> None:
|
||||||
|
with _locked(path):
|
||||||
|
_replace(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _cached(path: str) -> ForgeFacts | None:
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
if now() - data["fetched_at"] >= CACHE_TTL:
|
||||||
|
return None
|
||||||
|
return ForgeFacts(
|
||||||
|
merged_heads=data["merged_heads"],
|
||||||
|
open_heads=data["open_heads"],
|
||||||
|
protected=set(data["protected"]),
|
||||||
|
)
|
||||||
|
except (OSError, ValueError, KeyError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _store(path: str, facts: ForgeFacts) -> None:
|
||||||
|
data = {
|
||||||
|
"fetched_at": now(),
|
||||||
|
"merged_heads": facts.merged_heads,
|
||||||
|
"open_heads": facts.open_heads,
|
||||||
|
"protected": sorted(facts.protected),
|
||||||
|
}
|
||||||
|
_write(path, json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
|
def _unavailable(reason: str | None) -> tuple[ForgeFacts, str]:
|
||||||
|
return ForgeFacts(unavailable=reason), f"unavailable: {reason}"
|
||||||
|
|
||||||
|
|
||||||
|
def forge_facts(
|
||||||
|
urls: dict[str, str], token: str | None, reason: str | None
|
||||||
|
) -> tuple[ForgeFacts, str]:
|
||||||
|
"""
|
||||||
|
Forge facts of one repository from the cache, or live once the cache is stale.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
urls: remote name -> URL.
|
||||||
|
token: GitHub token from the Secret Service, or None.
|
||||||
|
reason: why the token is None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The facts and their source: none, cached, live or unavailable: <why>.
|
||||||
|
"""
|
||||||
|
slugs = github_slugs(urls)
|
||||||
|
if not slugs:
|
||||||
|
return ForgeFacts(), "none"
|
||||||
|
invalid = [f"{o}/{n}" for o, n in slugs if not (SLUG.match(o) and SLUG.match(n))]
|
||||||
|
if invalid:
|
||||||
|
return _unavailable(f"invalid GitHub slug {invalid[0]}")
|
||||||
|
merged, label = ForgeFacts(), "cached"
|
||||||
|
for owner, name in slugs:
|
||||||
|
path = _cache_path(owner, name)
|
||||||
|
facts = _cached(path)
|
||||||
|
if facts is None:
|
||||||
|
if token is None:
|
||||||
|
return _unavailable(reason)
|
||||||
|
facts = facts_with_token([(owner, name)], token)
|
||||||
|
if facts.unavailable:
|
||||||
|
return _unavailable(facts.unavailable)
|
||||||
|
try:
|
||||||
|
_store(path, facts)
|
||||||
|
label = "live" if label == "cached" else label
|
||||||
|
except OSError as exc:
|
||||||
|
label = f"live: cache not written ({exc})"
|
||||||
|
merged.merged_heads.update(facts.merged_heads)
|
||||||
|
merged.open_heads.update(facts.open_heads)
|
||||||
|
merged.protected |= facts.protected
|
||||||
|
return merged, label
|
||||||
|
|
||||||
|
|
||||||
|
def _missing(root: str, known: bool, scanned_at: str) -> dict:
|
||||||
|
return {
|
||||||
|
"path": root,
|
||||||
|
"remote": None,
|
||||||
|
"default": None,
|
||||||
|
"missing": True,
|
||||||
|
"known": known,
|
||||||
|
"forge": None,
|
||||||
|
"scanned_at": scanned_at,
|
||||||
|
"warnings": [],
|
||||||
|
"items": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _git_dir(
|
||||||
|
root: str, registrations: gc.Registrations, worktree: str | None
|
||||||
|
) -> str | None:
|
||||||
|
if worktree is None:
|
||||||
|
return None
|
||||||
|
if worktree == root:
|
||||||
|
return os.path.dirname(registrations[0])
|
||||||
|
return gc._linked_meta(registrations, worktree)
|
||||||
|
|
||||||
|
|
||||||
|
def _flow(git_dir: str | None, item: dict, warnings: list[str]) -> dict | None:
|
||||||
|
found = flow.read_flow(git_dir) if git_dir else None
|
||||||
|
if found is None or found.source == "none":
|
||||||
|
return None
|
||||||
|
warnings += [
|
||||||
|
f"flow of {item['worktree']}: {warning}"
|
||||||
|
for warning in found.warnings + flow.mismatch(found, item["branch"])
|
||||||
|
]
|
||||||
|
return {"pull": found.pull, "push": found.push, "source": found.source}
|
||||||
|
|
||||||
|
|
||||||
|
def _scan(
|
||||||
|
root: str,
|
||||||
|
known: bool,
|
||||||
|
token: str | None,
|
||||||
|
reason: str | None,
|
||||||
|
containers: list[gc.Container] | None,
|
||||||
|
host_ns: bool,
|
||||||
|
) -> dict:
|
||||||
|
scanned_at = _iso(now())
|
||||||
|
entry = _missing(root, known, scanned_at)
|
||||||
|
if not os.path.exists(os.path.join(root, ".git")):
|
||||||
|
return entry
|
||||||
|
sources: list[str] = []
|
||||||
|
|
||||||
|
def source(urls: dict[str, str]) -> ForgeFacts:
|
||||||
|
facts, label = forge_facts(urls, token, reason)
|
||||||
|
sources.append(label)
|
||||||
|
return facts
|
||||||
|
|
||||||
|
entry["missing"] = False
|
||||||
|
try:
|
||||||
|
repo = gc.plan_repo(
|
||||||
|
root,
|
||||||
|
containers,
|
||||||
|
host_ns,
|
||||||
|
merged_into=[],
|
||||||
|
sizes=False,
|
||||||
|
min_age=None,
|
||||||
|
scope="all",
|
||||||
|
activity=True,
|
||||||
|
hooks={},
|
||||||
|
fetch=False,
|
||||||
|
forge_source=source,
|
||||||
|
)
|
||||||
|
registrations = gc._registrations(root)
|
||||||
|
except GitBaseError as exc:
|
||||||
|
entry["warnings"] = [f"scan failed: {str(exc).strip().splitlines()[0]}"]
|
||||||
|
return entry
|
||||||
|
owner = next((r for r in repo.remotes if repo.default.startswith(f"{r}/")), None)
|
||||||
|
items = [asdict(item) for item in repo.items]
|
||||||
|
for item in items:
|
||||||
|
git_dir = _git_dir(root, registrations, item["worktree"])
|
||||||
|
item["flow"] = _flow(git_dir, item, repo.warnings)
|
||||||
|
entry.update(
|
||||||
|
remote=repo.urls[repo.remotes[0]] if repo.remotes else None,
|
||||||
|
default=repo.default.removeprefix(f"{owner}/") if owner else repo.default,
|
||||||
|
forge=sources[0],
|
||||||
|
warnings=repo.warnings,
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def _owner_root(checkout: str) -> str | None:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(checkout, ".git"), encoding="utf-8") as fh:
|
||||||
|
gitdir = fh.read().strip().removeprefix("gitdir: ")
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(os.path.realpath(os.path.join(checkout, gitdir)))
|
||||||
|
if os.path.basename(parent) != "worktrees":
|
||||||
|
return None
|
||||||
|
common = os.path.dirname(parent)
|
||||||
|
return os.path.dirname(common) if os.path.basename(common) == ".git" else common
|
||||||
|
|
||||||
|
|
||||||
|
def _shared_owners(worktrees_root: str) -> list[str]:
|
||||||
|
owners: set[str] = set()
|
||||||
|
base = worktrees_root.rstrip("/").count("/")
|
||||||
|
for path, dirs, files in os.walk(worktrees_root):
|
||||||
|
dirs.sort()
|
||||||
|
owner = _owner_root(path) if ".git" in files else None
|
||||||
|
if owner:
|
||||||
|
owners.add(owner)
|
||||||
|
if (
|
||||||
|
".git" in files
|
||||||
|
or ".git" in dirs
|
||||||
|
or path.count("/") - base >= WORKTREE_DEPTH
|
||||||
|
):
|
||||||
|
dirs.clear()
|
||||||
|
return sorted(owners)
|
||||||
|
|
||||||
|
|
||||||
|
def branch_tree(
|
||||||
|
repo_dirs: list[str], *, fmt: str, output: str | None, worktrees_root: str
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Print or write the branch and worktree state of every repository, without fetching.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_dirs: directories of the repositories pkgmgr knows.
|
||||||
|
fmt: json or yaml.
|
||||||
|
output: file to write atomically; None prints to stdout.
|
||||||
|
worktrees_root: shared worktree root whose unknown owners are added.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
0 on success, 1 when the output could not be written.
|
||||||
|
"""
|
||||||
|
known = list(dict.fromkeys(os.path.realpath(d) for d in repo_dirs))
|
||||||
|
orphans = [d for d in _shared_owners(worktrees_root) if d not in known]
|
||||||
|
token, reason = secret_service_token()
|
||||||
|
host_ns = gc.host_pid_namespace()
|
||||||
|
containers = gc.list_compose_containers()
|
||||||
|
data = {
|
||||||
|
"generated_at": _iso(now()),
|
||||||
|
"host_pid_namespace": host_ns,
|
||||||
|
"delete_ready": token is not None,
|
||||||
|
"delete_reason": reason,
|
||||||
|
"repos": [
|
||||||
|
_scan(root, root in known, token, reason, containers, host_ns)
|
||||||
|
for root in known + orphans
|
||||||
|
],
|
||||||
|
}
|
||||||
|
text = (
|
||||||
|
yaml.safe_dump(data, sort_keys=False)
|
||||||
|
if fmt == "yaml"
|
||||||
|
else json.dumps(data, indent=2) + "\n"
|
||||||
|
)
|
||||||
|
if output is None:
|
||||||
|
sys.stdout.write(text)
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
_write(output, text)
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Cannot write {output}: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_tree_entry(repo_root: str) -> None:
|
||||||
|
"""
|
||||||
|
Rescan one repository into the tree file, if the file exists and holds it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_root: main worktree root of the repository.
|
||||||
|
|
||||||
|
Skipped outside the host PID namespace and when the file's entry is newer.
|
||||||
|
"""
|
||||||
|
path = tree_file()
|
||||||
|
if not os.path.isfile(path) or not gc.host_pid_namespace():
|
||||||
|
return
|
||||||
|
root = os.path.realpath(repo_root)
|
||||||
|
token, reason = secret_service_token()
|
||||||
|
fresh = _scan(root, True, token, reason, gc.list_compose_containers(), True)
|
||||||
|
with _locked(path):
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
index = next(
|
||||||
|
(i for i, entry in enumerate(data["repos"]) if entry["path"] == root), None
|
||||||
|
)
|
||||||
|
if index is None or data["repos"][index]["scanned_at"] > fresh["scanned_at"]:
|
||||||
|
return
|
||||||
|
fresh["known"] = data["repos"][index]["known"]
|
||||||
|
data["repos"][index] = fresh
|
||||||
|
_replace(path, json.dumps(data, indent=2) + "\n")
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from pkgmgr.core.git import run_git, GitError
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_base_branch(
|
|
||||||
preferred: str,
|
|
||||||
fallback: str,
|
|
||||||
cwd: str,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Resolve the base branch to use.
|
|
||||||
|
|
||||||
Try `preferred` first (default: main),
|
|
||||||
fall back to `fallback` (default: master).
|
|
||||||
|
|
||||||
Raise RuntimeError if neither exists.
|
|
||||||
"""
|
|
||||||
for candidate in (preferred, fallback):
|
|
||||||
try:
|
|
||||||
run_git(["rev-parse", "--verify", candidate], cwd=cwd)
|
|
||||||
return candidate
|
|
||||||
except GitError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Neither {preferred!r} nor {fallback!r} exist in this repository."
|
|
||||||
)
|
|
||||||
@@ -1,72 +1,38 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Helpers to generate changelog information from Git history.
|
Helpers to generate changelog information from Git history.
|
||||||
|
|
||||||
This module provides a small abstraction around `git log` so that
|
|
||||||
CLI commands can request a changelog between two refs (tags, branches,
|
|
||||||
commits) without dealing with raw subprocess calls.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from pkgmgr.core.git.queries import (
|
||||||
|
GitChangelogQueryError,
|
||||||
from pkgmgr.core.git import run_git, GitError
|
get_changelog,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_changelog(
|
def generate_changelog(
|
||||||
cwd: str,
|
cwd: str,
|
||||||
from_ref: Optional[str] = None,
|
from_ref: str | None = None,
|
||||||
to_ref: Optional[str] = None,
|
to_ref: str | None = None,
|
||||||
include_merges: bool = False,
|
include_merges: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate a plain-text changelog between two Git refs.
|
Generate a plain-text changelog between two Git refs.
|
||||||
|
|
||||||
Parameters
|
Returns a human-readable message instead of raising.
|
||||||
----------
|
|
||||||
cwd:
|
|
||||||
Repository directory in which to run Git commands.
|
|
||||||
from_ref:
|
|
||||||
Optional starting reference (exclusive). If provided together
|
|
||||||
with `to_ref`, the range `from_ref..to_ref` is used.
|
|
||||||
If only `from_ref` is given, the range `from_ref..HEAD` is used.
|
|
||||||
to_ref:
|
|
||||||
Optional end reference (inclusive). If omitted, `HEAD` is used.
|
|
||||||
include_merges:
|
|
||||||
If False (default), merge commits are filtered out.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
The output of `git log` formatted as a simple text changelog.
|
|
||||||
If no commits are found or Git fails, an explanatory message
|
|
||||||
is returned instead of raising.
|
|
||||||
"""
|
"""
|
||||||
# Determine the revision range
|
|
||||||
if to_ref is None:
|
if to_ref is None:
|
||||||
to_ref = "HEAD"
|
to_ref = "HEAD"
|
||||||
|
|
||||||
if from_ref:
|
rev_range = f"{from_ref}..{to_ref}" if from_ref else to_ref
|
||||||
rev_range = f"{from_ref}..{to_ref}"
|
|
||||||
else:
|
|
||||||
rev_range = to_ref
|
|
||||||
|
|
||||||
# Use a custom pretty format that includes tags/refs (%d)
|
|
||||||
cmd = [
|
|
||||||
"log",
|
|
||||||
"--pretty=format:%h %d %s",
|
|
||||||
]
|
|
||||||
if not include_merges:
|
|
||||||
cmd.append("--no-merges")
|
|
||||||
cmd.append(rev_range)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output = run_git(cmd, cwd=cwd)
|
output = get_changelog(
|
||||||
except GitError as exc:
|
cwd=cwd,
|
||||||
# Do not raise to the CLI, return a human-readable error instead.
|
from_ref=from_ref,
|
||||||
|
to_ref=to_ref,
|
||||||
|
include_merges=include_merges,
|
||||||
|
)
|
||||||
|
except GitChangelogQueryError as exc:
|
||||||
return (
|
return (
|
||||||
f"[ERROR] Failed to generate changelog in {cwd!r} "
|
f"[ERROR] Failed to generate changelog in {cwd!r} "
|
||||||
f"for range {rev_range!r}:\n{exc}"
|
f"for range {rev_range!r}:\n{exc}"
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"""Download GitHub code scanning results via the ``gh`` CLI.
|
||||||
|
|
||||||
|
Fetches all code scanning alerts (and the analysis metadata) for a
|
||||||
|
repository and writes them, plus a readable digest, into a timestamped
|
||||||
|
directory so they can be read and analysed offline. Authentication is
|
||||||
|
delegated to ``gh`` (credentials from its keyring/login).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from collections import Counter
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class CodeScanningError(RuntimeError):
|
||||||
|
"""Raised when code scanning results cannot be downloaded."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CodeScanningResult:
|
||||||
|
repo: str
|
||||||
|
output_dir: str
|
||||||
|
alert_count: int
|
||||||
|
files: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def _gh(args: list[str]) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(["gh", *args], capture_output=True, text=True, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_repo(repo: str | None) -> str:
|
||||||
|
if repo:
|
||||||
|
return repo
|
||||||
|
proc = _gh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"])
|
||||||
|
name = proc.stdout.strip()
|
||||||
|
if proc.returncode != 0 or not name:
|
||||||
|
raise CodeScanningError(
|
||||||
|
"could not resolve the current repository; run inside a GitHub "
|
||||||
|
"repo or pass --repo OWNER/REPO "
|
||||||
|
f"({proc.stderr.strip()})"
|
||||||
|
)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_json(endpoint: str, params: list[str] | None = None) -> Any:
|
||||||
|
args = ["api", endpoint, "--paginate"]
|
||||||
|
for param in params or []:
|
||||||
|
args += ["-f", param]
|
||||||
|
proc = _gh(args)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise CodeScanningError(
|
||||||
|
f"gh api {endpoint} failed: {proc.stderr.strip() or 'unknown error'}"
|
||||||
|
)
|
||||||
|
body = proc.stdout.strip()
|
||||||
|
return json.loads(body) if body else []
|
||||||
|
|
||||||
|
|
||||||
|
def _alert_row(alert: dict) -> str:
|
||||||
|
rule = alert.get("rule") or {}
|
||||||
|
instance = alert.get("most_recent_instance") or {}
|
||||||
|
location = instance.get("location") or {}
|
||||||
|
message = (instance.get("message") or {}).get("text", "").strip().replace("\n", " ")
|
||||||
|
severity = rule.get("security_severity_level") or rule.get("severity") or "unknown"
|
||||||
|
path = location.get("path", "?")
|
||||||
|
line = location.get("start_line", "?")
|
||||||
|
state = alert.get("state", "?")
|
||||||
|
rule_id = rule.get("id", "?")
|
||||||
|
return f"- [{severity}] {rule_id} — {path}:{line} ({state})\n {message}"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_summary(repo: str, generated_at: str, alerts: list[dict]) -> str:
|
||||||
|
by_severity: Counter = Counter()
|
||||||
|
by_state: Counter = Counter()
|
||||||
|
by_rule: Counter = Counter()
|
||||||
|
for alert in alerts:
|
||||||
|
rule = alert.get("rule") or {}
|
||||||
|
by_severity[
|
||||||
|
rule.get("security_severity_level") or rule.get("severity") or "unknown"
|
||||||
|
] += 1
|
||||||
|
by_state[alert.get("state", "unknown")] += 1
|
||||||
|
by_rule[rule.get("id", "unknown")] += 1
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"# Code scanning summary — {repo}",
|
||||||
|
"",
|
||||||
|
f"- Generated: {generated_at}",
|
||||||
|
f"- Total alerts: {len(alerts)}",
|
||||||
|
"",
|
||||||
|
"## By severity",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
lines += [f"- {sev}: {count}" for sev, count in by_severity.most_common()] or [
|
||||||
|
"- none"
|
||||||
|
]
|
||||||
|
lines += ["", "## By state", ""]
|
||||||
|
lines += [f"- {state}: {count}" for state, count in by_state.most_common()] or [
|
||||||
|
"- none"
|
||||||
|
]
|
||||||
|
lines += ["", "## By rule", ""]
|
||||||
|
lines += [f"- {rule}: {count}" for rule, count in by_rule.most_common()] or [
|
||||||
|
"- none"
|
||||||
|
]
|
||||||
|
lines += ["", "## Alerts", ""]
|
||||||
|
lines += [_alert_row(alert) for alert in alerts] or ["- none"]
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def download_code_scanning(
|
||||||
|
repo: str | None = None,
|
||||||
|
output_dir: str | None = None,
|
||||||
|
state: str | None = None,
|
||||||
|
) -> CodeScanningResult:
|
||||||
|
if not shutil.which("gh"):
|
||||||
|
raise CodeScanningError("the GitHub CLI 'gh' is not installed or not on PATH")
|
||||||
|
|
||||||
|
repo = _resolve_repo(repo)
|
||||||
|
repo_name = repo.rstrip("/").split("/")[-1]
|
||||||
|
|
||||||
|
if output_dir is None:
|
||||||
|
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
||||||
|
output_dir = os.path.join("/tmp", repo_name, "code-scanner", timestamp)
|
||||||
|
output_dir = os.path.abspath(os.path.expanduser(output_dir))
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
generated_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
alerts = _fetch_json(
|
||||||
|
f"repos/{repo}/code-scanning/alerts",
|
||||||
|
params=[f"state={state}"] if state else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
files: list[str] = []
|
||||||
|
|
||||||
|
def _write(name: str, content: str) -> None:
|
||||||
|
path = os.path.join(output_dir, name)
|
||||||
|
with open(path, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(content)
|
||||||
|
files.append(path)
|
||||||
|
|
||||||
|
_write("alerts.json", json.dumps(alerts, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
_write("summary.md", _build_summary(repo, generated_at, alerts))
|
||||||
|
|
||||||
|
try:
|
||||||
|
analyses = _fetch_json(f"repos/{repo}/code-scanning/analyses")
|
||||||
|
_write(
|
||||||
|
"analyses.json", json.dumps(analyses, indent=2, ensure_ascii=False) + "\n"
|
||||||
|
)
|
||||||
|
except CodeScanningError as exc:
|
||||||
|
_write("analyses.json", json.dumps({"error": str(exc)}, indent=2) + "\n")
|
||||||
|
|
||||||
|
return CodeScanningResult(
|
||||||
|
repo=repo,
|
||||||
|
output_dir=output_dir,
|
||||||
|
alert_count=len(alerts),
|
||||||
|
files=files,
|
||||||
|
)
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
import yaml
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def interactive_add(config,USER_CONFIG_PATH:str):
|
import yaml
|
||||||
|
|
||||||
|
from pkgmgr.core.config.save import save_user_config
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_add(config, USER_CONFIG_PATH: str):
|
||||||
"""Interactively prompt the user to add a new repository entry to the user config."""
|
"""Interactively prompt the user to add a new repository entry to the user config."""
|
||||||
print("Adding a new repository configuration entry.")
|
print("Adding a new repository configuration entry.")
|
||||||
new_entry = {}
|
new_entry = {}
|
||||||
new_entry["provider"] = input("Provider (e.g., github.com): ").strip()
|
new_entry["provider"] = input("Provider (e.g., github.com): ").strip()
|
||||||
new_entry["account"] = input("Account (e.g., yourusername): ").strip()
|
new_entry["account"] = input("Account (e.g., yourusername): ").strip()
|
||||||
new_entry["repository"] = input("Repository name (e.g., mytool): ").strip()
|
new_entry["repository"] = input("Repository name (e.g., mytool): ").strip()
|
||||||
new_entry["command"] = input("Command (optional, leave blank to auto-detect): ").strip()
|
new_entry["command"] = input(
|
||||||
|
"Command (optional, leave blank to auto-detect): "
|
||||||
|
).strip()
|
||||||
new_entry["description"] = input("Description (optional): ").strip()
|
new_entry["description"] = input("Description (optional): ").strip()
|
||||||
new_entry["replacement"] = input("Replacement (optional): ").strip()
|
new_entry["replacement"] = input("Replacement (optional): ").strip()
|
||||||
new_entry["alias"] = input("Alias (optional): ").strip()
|
new_entry["alias"] = input("Alias (optional): ").strip()
|
||||||
@@ -24,12 +30,12 @@ def interactive_add(config,USER_CONFIG_PATH:str):
|
|||||||
confirm = input("Add this entry to user config? (y/N): ").strip().lower()
|
confirm = input("Add this entry to user config? (y/N): ").strip().lower()
|
||||||
if confirm == "y":
|
if confirm == "y":
|
||||||
if os.path.exists(USER_CONFIG_PATH):
|
if os.path.exists(USER_CONFIG_PATH):
|
||||||
with open(USER_CONFIG_PATH, 'r') as f:
|
with open(USER_CONFIG_PATH) as f:
|
||||||
user_config = yaml.safe_load(f) or {}
|
user_config = yaml.safe_load(f) or {}
|
||||||
else:
|
else:
|
||||||
user_config = {"repositories": []}
|
user_config = {"repositories": []}
|
||||||
user_config.setdefault("repositories", [])
|
user_config.setdefault("repositories", [])
|
||||||
user_config["repositories"].append(new_entry)
|
user_config["repositories"].append(new_entry)
|
||||||
save_user_config(user_config,USER_CONFIG_PATH)
|
save_user_config(user_config, USER_CONFIG_PATH)
|
||||||
else:
|
else:
|
||||||
print("Entry not added.")
|
print("Entry not added.")
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Initialize user configuration by scanning the repositories base directory.
|
Initialize user configuration by scanning the repositories base directory.
|
||||||
|
|
||||||
@@ -14,7 +11,7 @@ with the expected structure:
|
|||||||
|
|
||||||
For each discovered repository, the function:
|
For each discovered repository, the function:
|
||||||
• derives provider, account, repository from the folder structure
|
• derives provider, account, repository from the folder structure
|
||||||
• (optionally) determines the latest commit hash via git log
|
• (optionally) determines the latest commit hash via git
|
||||||
• generates a unique CLI alias
|
• generates a unique CLI alias
|
||||||
• marks ignore=True for newly discovered repos
|
• marks ignore=True for newly discovered repos
|
||||||
• skips repos already known in defaults or user config
|
• skips repos already known in defaults or user config
|
||||||
@@ -23,16 +20,16 @@ For each discovered repository, the function:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
from typing import Any
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from pkgmgr.core.command.alias import generate_alias
|
from pkgmgr.core.command.alias import generate_alias
|
||||||
from pkgmgr.core.config.save import save_user_config
|
from pkgmgr.core.config.save import save_user_config
|
||||||
|
from pkgmgr.core.git.queries import get_latest_commit
|
||||||
|
|
||||||
|
|
||||||
def config_init(
|
def config_init(
|
||||||
user_config: Dict[str, Any],
|
user_config: dict[str, Any],
|
||||||
defaults_config: Dict[str, Any],
|
defaults_config: dict[str, Any],
|
||||||
bin_dir: str,
|
bin_dir: str,
|
||||||
user_config_path: str,
|
user_config_path: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -45,7 +42,7 @@ def config_init(
|
|||||||
# Announce where we will write the result
|
# Announce where we will write the result
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
print("============================================================")
|
print("============================================================")
|
||||||
print(f"[INIT] Writing user configuration to:")
|
print("[INIT] Writing user configuration to:")
|
||||||
print(f" {user_config_path}")
|
print(f" {user_config_path}")
|
||||||
print("============================================================")
|
print("============================================================")
|
||||||
|
|
||||||
@@ -53,9 +50,9 @@ def config_init(
|
|||||||
defaults_config["directories"]["repositories"]
|
defaults_config["directories"]["repositories"]
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"[INIT] Scanning repository base directory:")
|
print("[INIT] Scanning repository base directory:")
|
||||||
print(f" {repositories_base_dir}")
|
print(f" {repositories_base_dir}")
|
||||||
print("")
|
print()
|
||||||
|
|
||||||
if not os.path.isdir(repositories_base_dir):
|
if not os.path.isdir(repositories_base_dir):
|
||||||
print(f"[ERROR] Base directory does not exist: {repositories_base_dir}")
|
print(f"[ERROR] Base directory does not exist: {repositories_base_dir}")
|
||||||
@@ -107,36 +104,33 @@ def config_init(
|
|||||||
# Already known?
|
# Already known?
|
||||||
if key in default_keys:
|
if key in default_keys:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
print(f"[SKIP] (defaults) {provider}/{account}/{repo_name}")
|
print(
|
||||||
|
f"[SKIP] (defaults) {provider}/{account}/{repo_name}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if key in existing_keys:
|
if key in existing_keys:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
print(f"[SKIP] (user-config) {provider}/{account}/{repo_name}")
|
print(
|
||||||
|
f"[SKIP] (user-config) {provider}/{account}/{repo_name}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"[ADD] {provider}/{account}/{repo_name}")
|
print(f"[ADD] {provider}/{account}/{repo_name}")
|
||||||
|
|
||||||
# Determine commit hash
|
# Determine commit hash via git query
|
||||||
try:
|
verified_commit = get_latest_commit(repo_path) or ""
|
||||||
result = subprocess.run(
|
if verified_commit:
|
||||||
["git", "log", "-1", "--format=%H"],
|
print(f"[INFO] Latest commit: {verified_commit}")
|
||||||
cwd=repo_path,
|
else:
|
||||||
stdout=subprocess.PIPE,
|
print(
|
||||||
stderr=subprocess.PIPE,
|
"[WARN] Could not read commit (not a git repo or no commits)."
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
)
|
||||||
verified = result.stdout.strip()
|
|
||||||
print(f"[INFO] Latest commit: {verified}")
|
|
||||||
except Exception as exc:
|
|
||||||
verified = ""
|
|
||||||
print(f"[WARN] Could not read commit: {exc}")
|
|
||||||
|
|
||||||
entry = {
|
entry: dict[str, Any] = {
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"account": account,
|
"account": account,
|
||||||
"repository": repo_name,
|
"repository": repo_name,
|
||||||
"verified": {"commit": verified},
|
"verified": {"commit": verified_commit},
|
||||||
"ignore": True,
|
"ignore": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +150,7 @@ def config_init(
|
|||||||
|
|
||||||
new_entries.append(entry)
|
new_entries.append(entry)
|
||||||
|
|
||||||
print("") # blank line between accounts
|
print() # blank line between accounts
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Summary
|
# Summary
|
||||||
@@ -173,7 +167,7 @@ def config_init(
|
|||||||
if new_entries:
|
if new_entries:
|
||||||
user_config.setdefault("repositories", []).extend(new_entries)
|
user_config.setdefault("repositories", []).extend(new_entries)
|
||||||
save_user_config(user_config, user_config_path)
|
save_user_config(user_config, user_config_path)
|
||||||
print(f"[SAVE] Wrote user configuration to:")
|
print("[SAVE] Wrote user configuration to:")
|
||||||
print(f" {user_config_path}")
|
print(f" {user_config_path}")
|
||||||
else:
|
else:
|
||||||
print("[INFO] No new repositories were added.")
|
print("[INFO] No new repositories were added.")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from pkgmgr.core.config.load import load_config
|
from pkgmgr.core.config.load import load_config
|
||||||
|
|
||||||
|
|
||||||
def show_config(selected_repos, user_config_path, full_config=False):
|
def show_config(selected_repos, user_config_path, full_config=False):
|
||||||
"""Display configuration for one or more repositories, or the entire merged config."""
|
"""Display configuration for one or more repositories, or the entire merged config."""
|
||||||
if full_config:
|
if full_config:
|
||||||
@@ -8,8 +10,10 @@ def show_config(selected_repos, user_config_path, full_config=False):
|
|||||||
print(yaml.dump(merged, default_flow_style=False))
|
print(yaml.dump(merged, default_flow_style=False))
|
||||||
else:
|
else:
|
||||||
for repo in selected_repos:
|
for repo in selected_repos:
|
||||||
identifier = f'{repo.get("provider")}/{repo.get("account")}/{repo.get("repository")}'
|
identifier = (
|
||||||
|
f"{repo.get('provider')}/{repo.get('account')}/{repo.get('repository')}"
|
||||||
|
)
|
||||||
print(f"Repository: {identifier}")
|
print(f"Repository: {identifier}")
|
||||||
for key, value in repo.items():
|
for key, value in repo.items():
|
||||||
print(f" {key}: {value}")
|
print(f" {key}: {value}")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
File renamed without changes.
@@ -0,0 +1,4 @@
|
|||||||
|
from .render import render_text
|
||||||
|
from .report import disk_report
|
||||||
|
|
||||||
|
__all__ = ["disk_report", "render_text"]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pkgmgr.actions.branch.gc import _inside
|
||||||
|
|
||||||
|
|
||||||
|
def _mount(path: str) -> str:
|
||||||
|
path = os.path.realpath(path)
|
||||||
|
while not os.path.ismount(path):
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def balance(paths: list[str], items: list[tuple[str, int]]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Set what the report attributes against each filesystem's capacity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paths: the roots and repository roots; each one's filesystem gets a row.
|
||||||
|
items: (path, allocated bytes) of every listed entry; an entry inside
|
||||||
|
another listed entry is counted only through the outer one.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Per filesystem (grouped by st_dev): its mount point, total and free
|
||||||
|
bytes (as available to unprivileged users) and the attributed bytes.
|
||||||
|
"""
|
||||||
|
filesystems: dict[int, dict] = {}
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
dev = os.stat(path).st_dev
|
||||||
|
if dev in filesystems:
|
||||||
|
continue
|
||||||
|
vfs = os.statvfs(path)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
filesystems[dev] = {
|
||||||
|
"filesystem": _mount(path),
|
||||||
|
"total_bytes": vfs.f_blocks * vfs.f_frsize,
|
||||||
|
"free_bytes": vfs.f_bavail * vfs.f_frsize,
|
||||||
|
"attributed_bytes": 0,
|
||||||
|
}
|
||||||
|
kept: list[str] = []
|
||||||
|
for path, size in sorted((os.path.realpath(p), s) for p, s in items):
|
||||||
|
if any(_inside(path, outer) for outer in kept):
|
||||||
|
continue
|
||||||
|
kept.append(path)
|
||||||
|
try:
|
||||||
|
dev = os.lstat(path).st_dev
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if dev in filesystems:
|
||||||
|
filesystems[dev]["attributed_bytes"] += size
|
||||||
|
return sorted(filesystems.values(), key=lambda fs: fs["filesystem"])
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .walk import BLOCK
|
||||||
|
|
||||||
|
ELF_MAGIC = b"\x7fELF"
|
||||||
|
ELF_HEADER = 18
|
||||||
|
ET_CORE = 4
|
||||||
|
BYTE_ORDER = {1: "little", 2: "big"}
|
||||||
|
|
||||||
|
|
||||||
|
def is_core_dump(path: str) -> bool:
|
||||||
|
try:
|
||||||
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
head = os.read(fd, ELF_HEADER)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
order = BYTE_ORDER.get(head[5]) if len(head) == ELF_HEADER else None
|
||||||
|
return (
|
||||||
|
head[:4] == ELF_MAGIC
|
||||||
|
and order is not None
|
||||||
|
and int.from_bytes(head[16:18], order) == ET_CORE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def core_dumps(candidates: list[str]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Keep the candidates whose ELF header says ET_CORE.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
candidates: paths of regular files named like core dumps.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
One entry per dump with its allocated and apparent size and mtime,
|
||||||
|
biggest first.
|
||||||
|
"""
|
||||||
|
dumps: dict[str, dict] = {}
|
||||||
|
for path in candidates:
|
||||||
|
real = os.path.realpath(path)
|
||||||
|
if real in dumps or not is_core_dump(path):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
st = os.lstat(path)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
dumps[real] = {
|
||||||
|
"path": path,
|
||||||
|
"size_bytes": st.st_blocks * BLOCK,
|
||||||
|
"apparent_bytes": st.st_size,
|
||||||
|
"mtime": st.st_mtime,
|
||||||
|
}
|
||||||
|
return sorted(dumps.values(), key=lambda d: (-d["size_bytes"], d["path"]))
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from pkgmgr.actions.branch.gc import _describe, _docker
|
||||||
|
|
||||||
|
DF_ARGS = ["system", "df", "--format", "{{json .}}"]
|
||||||
|
|
||||||
|
|
||||||
|
def _count(value: object) -> int | None:
|
||||||
|
text = str(value)
|
||||||
|
return int(text) if text.isdigit() else None
|
||||||
|
|
||||||
|
|
||||||
|
def docker_usage() -> tuple[list[dict] | None, str | None]:
|
||||||
|
"""
|
||||||
|
Read `docker system df` per type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The rows (type, total and active counts, size and reclaimable as docker
|
||||||
|
prints them) and None, or None and a warning when docker is missing,
|
||||||
|
fails or prints something else than JSON lines.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
out = _docker(DF_ARGS)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None, "docker unavailable: not installed"
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
return None, f"docker unavailable: {_describe(exc)}"
|
||||||
|
rows: list[dict] = []
|
||||||
|
for line in out.splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = json.loads(line)
|
||||||
|
except ValueError:
|
||||||
|
data = None
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None, "docker system df: output is not JSON lines"
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"type": data.get("Type"),
|
||||||
|
"total": _count(data.get("TotalCount")),
|
||||||
|
"active": _count(data.get("Active")),
|
||||||
|
"size": data.get("Size"),
|
||||||
|
"reclaimable": data.get("Reclaimable"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows, None
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
from pkgmgr.actions.branch.gc import _describe, _git, _has_submodules
|
||||||
|
from pkgmgr.core.git.errors import GitBaseError
|
||||||
|
from pkgmgr.core.git.queries import FILTER_CONFIG, local_config
|
||||||
|
from pkgmgr.core.git.run import run
|
||||||
|
|
||||||
|
REF_NAMESPACES = ("refs/heads", "refs/remotes", "refs/tags")
|
||||||
|
PEELED = "%(if)%(*objectname)%(then)%(*objectname)%(else)%(objectname)%(end)"
|
||||||
|
UNMERGED = "unmerged"
|
||||||
|
DELETED = "deleted"
|
||||||
|
DIRECTORY = "directory"
|
||||||
|
SPECIAL = "special"
|
||||||
|
UNHASHABLE = {
|
||||||
|
DIRECTORY: "untracked directory or gitlink",
|
||||||
|
SPECIAL: "not a regular file",
|
||||||
|
}
|
||||||
|
MAX_REASONS = 5
|
||||||
|
|
||||||
|
|
||||||
|
def _tips(repo: str) -> list[str]:
|
||||||
|
out = _git(["for-each-ref", f"--format={PEELED}", *REF_NAMESPACES], repo)
|
||||||
|
return sorted(set(out.split()))
|
||||||
|
|
||||||
|
|
||||||
|
def _status_entries(tree: str) -> list[tuple[str, str | None]]:
|
||||||
|
raw = _git(
|
||||||
|
[
|
||||||
|
"status",
|
||||||
|
"--porcelain=v2",
|
||||||
|
"-z",
|
||||||
|
"--untracked-files=all",
|
||||||
|
"--no-renames",
|
||||||
|
],
|
||||||
|
tree,
|
||||||
|
)
|
||||||
|
entries: list[tuple[str, str | None]] = []
|
||||||
|
for record in raw.split("\0"):
|
||||||
|
kind = record[:1]
|
||||||
|
if kind == "1":
|
||||||
|
parts = record.split(" ", 8)
|
||||||
|
entries.append((parts[8], parts[7]))
|
||||||
|
elif kind == "u":
|
||||||
|
entries.append((record.split(" ", 10)[10], UNMERGED))
|
||||||
|
elif kind == "?":
|
||||||
|
entries.append((record[2:], None))
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _hashes(tree: str, paths: list[str]) -> dict[str, str]:
|
||||||
|
hashes: dict[str, str] = {}
|
||||||
|
files: list[str] = []
|
||||||
|
for path in paths:
|
||||||
|
full = os.path.join(tree, path)
|
||||||
|
try:
|
||||||
|
mode = os.lstat(full).st_mode
|
||||||
|
except FileNotFoundError:
|
||||||
|
hashes[path] = DELETED
|
||||||
|
continue
|
||||||
|
if stat.S_ISREG(mode):
|
||||||
|
files.append(path)
|
||||||
|
elif stat.S_ISLNK(mode):
|
||||||
|
hashes[path] = run(
|
||||||
|
["hash-object", "-t", "blob", "--no-filters", "--stdin"],
|
||||||
|
cwd=tree,
|
||||||
|
scan=True,
|
||||||
|
input_text=os.readlink(full),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
hashes[path] = DIRECTORY if stat.S_ISDIR(mode) else SPECIAL
|
||||||
|
if files:
|
||||||
|
out = run(
|
||||||
|
["hash-object", "--no-filters", "--stdin-paths"],
|
||||||
|
cwd=tree,
|
||||||
|
scan=True,
|
||||||
|
input_text="".join(f"{path}\n" for path in files),
|
||||||
|
)
|
||||||
|
hashes.update(zip(files, out.split("\n")))
|
||||||
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
|
def _absent(
|
||||||
|
repo: str, tips: list[str], wanted: list[tuple[str, set[str]]]
|
||||||
|
) -> list[str]:
|
||||||
|
query = "".join(f"{tip}:{path}\n" for path, _ in wanted for tip in tips)
|
||||||
|
if not query:
|
||||||
|
return [path for path, _ in wanted]
|
||||||
|
found = run(
|
||||||
|
["cat-file", "--batch-check=%(objectname)"],
|
||||||
|
cwd=repo,
|
||||||
|
scan=True,
|
||||||
|
input_text=query,
|
||||||
|
).split("\n")
|
||||||
|
width = len(tips)
|
||||||
|
return [
|
||||||
|
path
|
||||||
|
for index, (path, blobs) in enumerate(wanted)
|
||||||
|
if blobs - set(found[index * width : (index + 1) * width])
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _reachable(repo: str, head: str | None) -> bool:
|
||||||
|
if not head:
|
||||||
|
return False
|
||||||
|
return bool(
|
||||||
|
_git(["for-each-ref", "--count=1", f"--contains={head}", *REF_NAMESPACES], repo)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _unpreserved(
|
||||||
|
repo: str, record: dict, tips: list[str]
|
||||||
|
) -> tuple[int | None, list[str]]:
|
||||||
|
tree = record["path"]
|
||||||
|
if _has_submodules(tree):
|
||||||
|
return None, ["submodules, whose config the scan cannot vet"]
|
||||||
|
unsafe = local_config(tree, FILTER_CONFIG)
|
||||||
|
if unsafe:
|
||||||
|
return None, [f"{', '.join(unsafe)} in the repository config"]
|
||||||
|
entries = _status_entries(tree)
|
||||||
|
reasons: list[str] = []
|
||||||
|
if entries and record["branch"] is None and not _reachable(repo, record["head"]):
|
||||||
|
reasons.append("detached HEAD reachable from no ref")
|
||||||
|
hashable: list[tuple[str, str | None]] = []
|
||||||
|
for path, index_blob in entries:
|
||||||
|
if "\n" in path:
|
||||||
|
reasons.append(f"{path!r}: path with a newline")
|
||||||
|
elif index_blob == UNMERGED:
|
||||||
|
reasons.append(f"{path}: unmerged")
|
||||||
|
else:
|
||||||
|
hashable.append((path, index_blob))
|
||||||
|
hashes = _hashes(tree, [path for path, _ in hashable])
|
||||||
|
wanted: list[tuple[str, set[str]]] = []
|
||||||
|
for path, index_blob in hashable:
|
||||||
|
current = hashes[path]
|
||||||
|
if current in UNHASHABLE:
|
||||||
|
reasons.append(f"{path}: {UNHASHABLE[current]}")
|
||||||
|
continue
|
||||||
|
blobs = {current} - {DELETED}
|
||||||
|
if index_blob and set(index_blob) != {"0"}:
|
||||||
|
blobs.add(index_blob)
|
||||||
|
if blobs:
|
||||||
|
wanted.append((path, blobs))
|
||||||
|
reasons += [f"{path}: content in no ref" for path in _absent(repo, tips, wanted)]
|
||||||
|
return len(entries), reasons
|
||||||
|
|
||||||
|
|
||||||
|
def preserved(repo: str, records: list[dict]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Decide for each dirty worktree whether all its work survives its deletion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo: main worktree root of the repository.
|
||||||
|
records: its worktree records from list_worktrees.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
One entry per dirty or unreadable worktree; `preserved` is true only
|
||||||
|
when every changed, staged and untracked file's content is a blob at
|
||||||
|
the same path of some branch, remote or tag tip, and a detached HEAD is
|
||||||
|
reachable from a ref. `reasons` lists what is not, capped.
|
||||||
|
"""
|
||||||
|
live = [r for r in records if not r["orphaned"] and not r["missing"]]
|
||||||
|
tips = _tips(repo) if live else []
|
||||||
|
results: list[dict] = []
|
||||||
|
for record in live:
|
||||||
|
try:
|
||||||
|
paths, reasons = _unpreserved(repo, record, tips)
|
||||||
|
except (GitBaseError, OSError, UnicodeError) as exc:
|
||||||
|
paths, reasons = None, [f"status failed: {_describe(exc)}"]
|
||||||
|
if paths == 0:
|
||||||
|
continue
|
||||||
|
extra = len(reasons) - MAX_REASONS
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"repo": repo,
|
||||||
|
"path": record["path"],
|
||||||
|
"branch": record["branch"],
|
||||||
|
"paths": paths,
|
||||||
|
"preserved": not reasons,
|
||||||
|
"reasons": reasons[:MAX_REASONS]
|
||||||
|
+ ([f"... {extra} more"] if extra > 0 else []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pkgmgr.actions.branch.gc import human_size
|
||||||
|
|
||||||
|
UNITS = (("d", 86400), ("h", 3600), ("m", 60))
|
||||||
|
DOCKER_COLUMNS = ("type", "total", "active", "size", "reclaimable")
|
||||||
|
|
||||||
|
|
||||||
|
def _size(value: int | None) -> str:
|
||||||
|
return "-" if value is None else human_size(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _age(now: float, stamp: float | None) -> str:
|
||||||
|
if not stamp:
|
||||||
|
return "-"
|
||||||
|
seconds = max(0.0, now - stamp)
|
||||||
|
for unit, length in UNITS:
|
||||||
|
if seconds >= length:
|
||||||
|
return f"{seconds / length:.1f}{unit}"
|
||||||
|
return f"{seconds:.0f}s"
|
||||||
|
|
||||||
|
|
||||||
|
def _section(title: str, headers: list[str], rows: list[list[str]]) -> list[str]:
|
||||||
|
if not rows:
|
||||||
|
return [f"== {title}", "(none)"]
|
||||||
|
widths = [max(len(cell) for cell in column) for column in zip(headers, *rows)]
|
||||||
|
return [f"== {title}"] + [
|
||||||
|
" ".join(cell.ljust(width) for cell, width in zip(row, widths)).rstrip()
|
||||||
|
for row in (headers, *rows)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _branch(tree: dict) -> str:
|
||||||
|
if tree["orphaned"]:
|
||||||
|
return "-"
|
||||||
|
return tree["branch"] or "(detached)"
|
||||||
|
|
||||||
|
|
||||||
|
def _state(tree: dict) -> str:
|
||||||
|
if tree["orphaned"]:
|
||||||
|
return "orphaned"
|
||||||
|
return "missing" if tree["missing"] else "linked"
|
||||||
|
|
||||||
|
|
||||||
|
def render_text(report: dict) -> str:
|
||||||
|
"""
|
||||||
|
Render a disk_report result as aligned text tables.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
report: the dict disk_report returned.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tables, one section per report key, newline-joined.
|
||||||
|
"""
|
||||||
|
now = report["generated_at"]
|
||||||
|
lines = _section(
|
||||||
|
"worktrees",
|
||||||
|
["SIZE", "ACTIVE", "FOREIGN", "STATE", "BRANCH", "PATH"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
_size(t["size_bytes"]),
|
||||||
|
_age(now, t["last_activity"]),
|
||||||
|
str(t["foreign_entries"]),
|
||||||
|
_state(t),
|
||||||
|
_branch(t),
|
||||||
|
t["path"],
|
||||||
|
]
|
||||||
|
for t in report["worktrees"]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
lines += _section(
|
||||||
|
"preserved (dirty worktrees)",
|
||||||
|
["PRESERVED", "PATHS", "BRANCH", "PATH", "REASONS"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"yes" if p["preserved"] else "no",
|
||||||
|
"-" if p["paths"] is None else str(p["paths"]),
|
||||||
|
p["branch"] or "(detached)",
|
||||||
|
p["path"],
|
||||||
|
"; ".join(p["reasons"]),
|
||||||
|
]
|
||||||
|
for p in report["preserved"]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for root in report["roots"]:
|
||||||
|
lines += _section(
|
||||||
|
f"root {root['root']} (over {human_size(report['threshold_bytes'])})",
|
||||||
|
["SIZE", "AGE", "KIND", "PATH"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
_size(c["size_bytes"]),
|
||||||
|
_age(now, c["newest_mtime"]),
|
||||||
|
c["kind"],
|
||||||
|
c["path"],
|
||||||
|
]
|
||||||
|
for c in root["children"]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
lines += _section(
|
||||||
|
"core dumps",
|
||||||
|
["SIZE", "APPARENT", "AGE", "PATH"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
_size(d["size_bytes"]),
|
||||||
|
_size(d["apparent_bytes"]),
|
||||||
|
_age(now, d["mtime"]),
|
||||||
|
d["path"],
|
||||||
|
]
|
||||||
|
for d in report["core_dumps"]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if report["docker"] is not None:
|
||||||
|
lines += _section(
|
||||||
|
"docker",
|
||||||
|
["TYPE", "TOTAL", "ACTIVE", "SIZE", "RECLAIMABLE"],
|
||||||
|
[[str(row[key]) for key in DOCKER_COLUMNS] for row in report["docker"]],
|
||||||
|
)
|
||||||
|
lines += _section(
|
||||||
|
"balance",
|
||||||
|
["FILESYSTEM", "TOTAL", "FREE", "ATTRIBUTED"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
fs["filesystem"],
|
||||||
|
_size(fs["total_bytes"]),
|
||||||
|
_size(fs["free_bytes"]),
|
||||||
|
_size(fs["attributed_bytes"]),
|
||||||
|
]
|
||||||
|
for fs in report["balance"]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
lines += [f"WARN {warning}" for warning in report["warnings"]]
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from pkgmgr.actions.branch.gc import _describe
|
||||||
|
from pkgmgr.core.git.errors import GitBaseError
|
||||||
|
|
||||||
|
from .balance import balance
|
||||||
|
from .core_dumps import core_dumps
|
||||||
|
from .docker_usage import docker_usage
|
||||||
|
from .preserved import preserved
|
||||||
|
from .roots import scan_root
|
||||||
|
from .worktrees import list_worktrees
|
||||||
|
|
||||||
|
|
||||||
|
def disk_report(
|
||||||
|
roots: list[str], repo_roots: list[str], threshold_bytes: int, docker: bool
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Inventory disk usage read-only: it deletes and writes nothing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
roots: directories whose direct children are measured.
|
||||||
|
repo_roots: main worktree roots of the repositories whose linked
|
||||||
|
worktrees are listed (pkgmgr passes every managed repository).
|
||||||
|
threshold_bytes: list only root children larger than this.
|
||||||
|
docker: also read `docker system df`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON-serialisable dict. Sizes are allocated bytes (st_blocks * 512,
|
||||||
|
sparse files and hard links counted as stored), measured without
|
||||||
|
following symlinks or leaving the filesystem a walk starts on; times
|
||||||
|
are epoch seconds.
|
||||||
|
generated_at: when the scan started.
|
||||||
|
threshold_bytes: as given.
|
||||||
|
worktrees: per linked worktree and orphaned `.claude/worktrees/agent-*`
|
||||||
|
dir: repo, path, branch, head, orphaned, missing, size_bytes,
|
||||||
|
last_activity (newest mtime inside or of its index/HEAD),
|
||||||
|
foreign_entries (not owned by the current uid, pruned at the first
|
||||||
|
per subtree), unreadable (directories that could not be listed).
|
||||||
|
preserved: per dirty worktree: repo, path, branch, paths (changed,
|
||||||
|
staged and untracked; None when status was not read), preserved,
|
||||||
|
reasons.
|
||||||
|
roots: per root: root, children over the threshold with path, kind
|
||||||
|
(worktree, git-repo, venv, node_modules, cache, core-dump, dir,
|
||||||
|
file), size_bytes, newest_mtime, unreadable; biggest first.
|
||||||
|
core_dumps: files named `core` or `core.<digits>` under the roots and
|
||||||
|
worktrees whose ELF e_type is ET_CORE: path, size_bytes,
|
||||||
|
apparent_bytes (st_size), mtime; biggest first.
|
||||||
|
docker: rows of type, total, active, size, reclaimable; None when not
|
||||||
|
asked for or unavailable.
|
||||||
|
balance: per filesystem of the roots and repository roots: filesystem
|
||||||
|
(mount point), total_bytes, free_bytes, attributed_bytes.
|
||||||
|
warnings: what could not be read.
|
||||||
|
"""
|
||||||
|
started = time.time()
|
||||||
|
warnings: list[str] = []
|
||||||
|
cores: list[str] = []
|
||||||
|
trees: list[dict] = []
|
||||||
|
kept: list[dict] = []
|
||||||
|
for repo in repo_roots:
|
||||||
|
try:
|
||||||
|
records = list_worktrees(repo, cores)
|
||||||
|
except (GitBaseError, OSError) as exc:
|
||||||
|
warnings.append(f"{repo}: worktrees not listed: {_describe(exc)}")
|
||||||
|
continue
|
||||||
|
trees += records
|
||||||
|
try:
|
||||||
|
kept += preserved(repo, records)
|
||||||
|
except (GitBaseError, OSError) as exc:
|
||||||
|
warnings.append(f"{repo}: preservation not checked: {_describe(exc)}")
|
||||||
|
scanned: list[dict] = []
|
||||||
|
for root in roots:
|
||||||
|
try:
|
||||||
|
scanned.append(scan_root(root, threshold_bytes, cores))
|
||||||
|
except OSError as exc:
|
||||||
|
warnings.append(f"{root}: not scanned: {_describe(exc)}")
|
||||||
|
dumps = core_dumps(cores)
|
||||||
|
usage = None
|
||||||
|
if docker:
|
||||||
|
usage, warning = docker_usage()
|
||||||
|
if warning:
|
||||||
|
warnings.append(warning)
|
||||||
|
items = [(t["path"], t["size_bytes"]) for t in trees if t["size_bytes"]]
|
||||||
|
items += [(c["path"], c["size_bytes"]) for s in scanned for c in s["children"]]
|
||||||
|
items += [(d["path"], d["size_bytes"]) for d in dumps]
|
||||||
|
return {
|
||||||
|
"generated_at": started,
|
||||||
|
"threshold_bytes": threshold_bytes,
|
||||||
|
"worktrees": trees,
|
||||||
|
"preserved": kept,
|
||||||
|
"roots": scanned,
|
||||||
|
"core_dumps": dumps,
|
||||||
|
"docker": usage,
|
||||||
|
"balance": balance([*roots, *repo_roots], items),
|
||||||
|
"warnings": warnings,
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
from .core_dumps import is_core_dump
|
||||||
|
from .walk import walk
|
||||||
|
|
||||||
|
CACHE_TAG = "CACHEDIR.TAG"
|
||||||
|
NODE_MODULES = "node_modules"
|
||||||
|
|
||||||
|
|
||||||
|
def _has(path: str, name: str, check=os.path.isfile) -> bool:
|
||||||
|
return check(os.path.join(path, name))
|
||||||
|
|
||||||
|
|
||||||
|
def _kind(path: str, st: os.stat_result) -> str:
|
||||||
|
name = os.path.basename(path)
|
||||||
|
if not stat.S_ISDIR(st.st_mode):
|
||||||
|
regular = stat.S_ISREG(st.st_mode)
|
||||||
|
return "core-dump" if regular and is_core_dump(path) else "file"
|
||||||
|
if name == NODE_MODULES:
|
||||||
|
return "node_modules"
|
||||||
|
if _has(path, ".git"):
|
||||||
|
return "worktree"
|
||||||
|
if _has(path, ".git", os.path.isdir) or (
|
||||||
|
_has(path, "HEAD") and _has(path, "objects", os.path.isdir)
|
||||||
|
):
|
||||||
|
return "git-repo"
|
||||||
|
if _has(path, "pyvenv.cfg"):
|
||||||
|
return "venv"
|
||||||
|
if _has(path, CACHE_TAG) or name.lower().endswith("cache"):
|
||||||
|
return "cache"
|
||||||
|
return "dir"
|
||||||
|
|
||||||
|
|
||||||
|
def scan_root(root: str, threshold_bytes: int, cores: list[str]) -> dict:
|
||||||
|
"""
|
||||||
|
Measure the direct children of one root on the root's own filesystem.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root: the directory to inventory.
|
||||||
|
threshold_bytes: keep only children whose allocated size is above it.
|
||||||
|
cores: receives the core dump candidates found while measuring.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The root with its children over the threshold: path, kind, allocated
|
||||||
|
size, newest mtime inside and unlistable directories, biggest first.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OSError: when the root itself cannot be listed.
|
||||||
|
"""
|
||||||
|
dev = os.stat(root).st_dev
|
||||||
|
children: list[dict] = []
|
||||||
|
with os.scandir(root) as entries:
|
||||||
|
listing = sorted(entries, key=lambda entry: entry.name)
|
||||||
|
for entry in listing:
|
||||||
|
try:
|
||||||
|
st = entry.stat(follow_symlinks=False)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if st.st_dev != dev:
|
||||||
|
continue
|
||||||
|
stats = walk(entry.path, dev)
|
||||||
|
cores.extend(stats.cores)
|
||||||
|
if stats.size_bytes > threshold_bytes:
|
||||||
|
children.append(
|
||||||
|
{
|
||||||
|
"path": entry.path,
|
||||||
|
"kind": _kind(entry.path, st),
|
||||||
|
"size_bytes": stats.size_bytes,
|
||||||
|
"newest_mtime": stats.newest_mtime,
|
||||||
|
"unreadable": stats.unreadable,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
children.sort(key=lambda child: (-child["size_bytes"], child["path"]))
|
||||||
|
return {"root": root, "children": children}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
BLOCK = 512
|
||||||
|
CORE_NAME = re.compile(r"core(\.\d+)?")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TreeStats:
|
||||||
|
size_bytes: int = 0
|
||||||
|
newest_mtime: float = 0.0
|
||||||
|
foreign_entries: int = 0
|
||||||
|
unreadable: int = 0
|
||||||
|
cores: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def is_foreign(path: str, st: os.stat_result) -> bool:
|
||||||
|
return st.st_uid != os.getuid()
|
||||||
|
|
||||||
|
|
||||||
|
def _account(
|
||||||
|
stats: TreeStats, path: str, st: os.stat_result, seen: set[tuple[int, int]]
|
||||||
|
) -> None:
|
||||||
|
if st.st_nlink > 1 and not stat.S_ISDIR(st.st_mode):
|
||||||
|
if (st.st_dev, st.st_ino) in seen:
|
||||||
|
return
|
||||||
|
seen.add((st.st_dev, st.st_ino))
|
||||||
|
stats.size_bytes += st.st_blocks * BLOCK
|
||||||
|
stats.newest_mtime = max(stats.newest_mtime, st.st_mtime)
|
||||||
|
if stat.S_ISREG(st.st_mode) and CORE_NAME.fullmatch(os.path.basename(path)):
|
||||||
|
stats.cores.append(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _children(path: str, stats: TreeStats) -> list[os.DirEntry]:
|
||||||
|
try:
|
||||||
|
with os.scandir(path) as entries:
|
||||||
|
return list(entries)
|
||||||
|
except OSError:
|
||||||
|
stats.unreadable += 1
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def walk(path: str, dev: int | None = None) -> TreeStats:
|
||||||
|
"""
|
||||||
|
Measure a tree like `du -x`: allocated blocks, symlinks never followed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: the tree root, measured itself as well.
|
||||||
|
dev: the filesystem (st_dev) to stay on; that of path when None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Allocated bytes (st_blocks * 512, each hard-linked inode once), the
|
||||||
|
newest mtime, entries not owned by the current uid (none counted below a
|
||||||
|
foreign one), directories that could not be listed, and the regular
|
||||||
|
files named `core` or `core.<digits>`.
|
||||||
|
"""
|
||||||
|
stats = TreeStats()
|
||||||
|
try:
|
||||||
|
top = os.lstat(path)
|
||||||
|
except OSError:
|
||||||
|
stats.unreadable += 1
|
||||||
|
return stats
|
||||||
|
dev = top.st_dev if dev is None else dev
|
||||||
|
if top.st_dev != dev:
|
||||||
|
return stats
|
||||||
|
seen: set[tuple[int, int]] = set()
|
||||||
|
stack: list[tuple[str, os.stat_result, bool]] = [(path, top, False)]
|
||||||
|
while stack:
|
||||||
|
current, st, pruned = stack.pop()
|
||||||
|
_account(stats, current, st, seen)
|
||||||
|
foreign = not pruned and is_foreign(current, st)
|
||||||
|
stats.foreign_entries += foreign
|
||||||
|
if not stat.S_ISDIR(st.st_mode):
|
||||||
|
continue
|
||||||
|
for entry in _children(current, stats):
|
||||||
|
try:
|
||||||
|
child = entry.stat(follow_symlinks=False)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if child.st_dev == dev:
|
||||||
|
stack.append((entry.path, child, pruned or foreign))
|
||||||
|
return stats
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pkgmgr.actions.branch.gc import _worktrees
|
||||||
|
|
||||||
|
from .walk import TreeStats, walk
|
||||||
|
|
||||||
|
AGENT_DIR = os.path.join(".claude", "worktrees")
|
||||||
|
AGENT_PREFIX = "agent-"
|
||||||
|
METADATA_FILES = ("index", "HEAD")
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_mtime(path: str) -> float:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(path, ".git"), encoding="utf-8") as fh:
|
||||||
|
gitdir = fh.read().strip().removeprefix("gitdir: ")
|
||||||
|
except OSError:
|
||||||
|
return 0.0
|
||||||
|
meta = os.path.normpath(os.path.join(path, gitdir))
|
||||||
|
stamps = [0.0]
|
||||||
|
for name in METADATA_FILES:
|
||||||
|
try:
|
||||||
|
stamps.append(os.stat(os.path.join(meta, name)).st_mtime)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return max(stamps)
|
||||||
|
|
||||||
|
|
||||||
|
def _record(
|
||||||
|
repo: str,
|
||||||
|
path: str,
|
||||||
|
branch: str | None,
|
||||||
|
head: str | None,
|
||||||
|
orphaned: bool,
|
||||||
|
cores: list[str],
|
||||||
|
) -> dict:
|
||||||
|
present = os.path.isdir(path)
|
||||||
|
stats = walk(path) if present else TreeStats()
|
||||||
|
cores.extend(stats.cores)
|
||||||
|
activity = max(stats.newest_mtime, _metadata_mtime(path)) if present else 0.0
|
||||||
|
return {
|
||||||
|
"repo": repo,
|
||||||
|
"path": path,
|
||||||
|
"branch": branch,
|
||||||
|
"head": head,
|
||||||
|
"orphaned": orphaned,
|
||||||
|
"missing": not present,
|
||||||
|
"size_bytes": stats.size_bytes if present else None,
|
||||||
|
"last_activity": activity or None,
|
||||||
|
"foreign_entries": stats.foreign_entries,
|
||||||
|
"unreadable": stats.unreadable,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _agent_dirs(repo: str) -> list[str]:
|
||||||
|
base = os.path.join(repo, AGENT_DIR)
|
||||||
|
try:
|
||||||
|
names = sorted(os.listdir(base))
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
os.path.join(base, name)
|
||||||
|
for name in names
|
||||||
|
if name.startswith(AGENT_PREFIX)
|
||||||
|
and os.path.isdir(os.path.join(base, name))
|
||||||
|
and not os.path.islink(os.path.join(base, name))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_worktrees(repo: str, cores: list[str]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Inventory the linked worktrees of one repository, orphaned agent dirs included.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo: main worktree root of the repository.
|
||||||
|
cores: receives the core dump candidates found while measuring.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
One record per linked worktree git lists and per `.claude/worktrees/agent-*`
|
||||||
|
directory it does not list (orphaned), sorted by path.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GitBaseError: when `git worktree list` fails.
|
||||||
|
"""
|
||||||
|
trees = [tree for tree in _worktrees(repo) if not tree.main]
|
||||||
|
listed = {os.path.realpath(tree.path) for tree in trees}
|
||||||
|
records = [
|
||||||
|
_record(repo, tree.path, tree.branch, tree.head, False, cores) for tree in trees
|
||||||
|
]
|
||||||
|
records += [
|
||||||
|
_record(repo, path, None, None, True, cores)
|
||||||
|
for path in _agent_dirs(repo)
|
||||||
|
if os.path.realpath(path) not in listed
|
||||||
|
]
|
||||||
|
return sorted(records, key=lambda record: record["path"])
|
||||||
Loaded 100 of 510 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user