data-refinery-cli 0.5.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. data_refinery_cli-0.5.2/.github/workflows/publish-stack.yml +175 -0
  2. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.github/workflows/tests.yml +12 -0
  3. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/CHANGELOG.md +16 -0
  4. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/CLAUDE.md +3 -1
  5. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/PKG-INFO +1 -1
  6. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/stack.py +50 -32
  7. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/docs/stack-image.md +15 -5
  8. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/pyproject.toml +1 -1
  9. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/uv.lock +1 -1
  10. data_refinery_cli-0.5.0/.github/workflows/publish-stack.yml +0 -95
  11. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/agent-config/SKILL.md +0 -0
  12. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  13. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/agent-config/scripts/show.sh +0 -0
  14. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/ask-colleague/SKILL.md +0 -0
  15. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  16. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  17. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  18. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
  19. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  20. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  21. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/cicd/SKILL.md +0 -0
  22. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  23. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  24. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  25. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  26. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  27. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/communicate/SKILL.md +0 -0
  28. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  29. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  30. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  31. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  32. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  33. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  34. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  35. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  36. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  37. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  38. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/run-tests/SKILL.md +0 -0
  39. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/run-tests/scripts/test.sh +0 -0
  40. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/sonarclaude/SKILL.md +0 -0
  41. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  42. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  43. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  44. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/think/SKILL.md +0 -0
  45. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/think/scripts/think.sh +0 -0
  46. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/version-bump/SKILL.md +0 -0
  47. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills/version-bump/scripts/bump.py +0 -0
  48. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.claude/skills.local.yaml.example +0 -0
  49. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.devague/current +0 -0
  50. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.devague/current_plan +0 -0
  51. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.devague/frames/data-refinery-cli-ships-the-storage-data-quality-i.json +0 -0
  52. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.devague/plans/data-refinery-cli-ships-the-storage-data-quality-i.json +0 -0
  53. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.flake8 +0 -0
  54. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.github/workflows/publish.yml +0 -0
  55. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.gitignore +0 -0
  56. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/.markdownlint-cli2.yaml +0 -0
  57. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/AGENTS.colleague.md +0 -0
  58. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/LICENSE +0 -0
  59. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/README.md +0 -0
  60. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/culture.yaml +0 -0
  61. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/__init__.py +0 -0
  62. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/__main__.py +0 -0
  63. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/__init__.py +0 -0
  64. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/__init__.py +0 -0
  65. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/cli.py +0 -0
  66. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/doctor.py +0 -0
  67. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/explain.py +0 -0
  68. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/learn.py +0 -0
  69. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/overview.py +0 -0
  70. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/quality.py +0 -0
  71. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/store.py +0 -0
  72. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_commands/whoami.py +0 -0
  73. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_errors.py +0 -0
  74. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/cli/_output.py +0 -0
  75. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/explain/__init__.py +0 -0
  76. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/explain/catalog.py +0 -0
  77. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/quality/__init__.py +0 -0
  78. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/quality/checks.py +0 -0
  79. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/store/__init__.py +0 -0
  80. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/store/backend.py +0 -0
  81. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/store/backends/__init__.py +0 -0
  82. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/store/backends/files.py +0 -0
  83. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/store/backends/mongo.py +0 -0
  84. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/store/backends/neo4j.py +0 -0
  85. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/data_refinery/store/envelope.py +0 -0
  86. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/docker-compose.yml +0 -0
  87. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/docs/contract.md +0 -0
  88. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/docs/plans/2026-06-20-data-refinery-cli-ships-the-storage-data-quality-i.md +0 -0
  89. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/docs/skill-sources.md +0 -0
  90. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/docs/specs/2026-06-20-data-refinery-cli-ships-the-storage-data-quality-i.md +0 -0
  91. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/sonar-project.properties +0 -0
  92. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/__init__.py +0 -0
  93. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/conftest.py +0 -0
  94. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_cli.py +0 -0
  95. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_cli_introspection.py +0 -0
  96. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_live_stack.py +0 -0
  97. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_no_optional_top_import.py +0 -0
  98. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_quality.py +0 -0
  99. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_scope_no_leak.py +0 -0
  100. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_stack.py +0 -0
  101. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_store_adapters.py +0 -0
  102. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_store_api.py +0 -0
  103. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_store_backends.py +0 -0
  104. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_store_cli.py +0 -0
  105. {data_refinery_cli-0.5.0 → data_refinery_cli-0.5.2}/tests/test_store_envelope.py +0 -0
@@ -0,0 +1,175 @@
1
+ name: Release — auto-tag + conditional stack publish
2
+
3
+ # On every push to main (a merged PR) this tags the commit `vX.Y.Z` from the
4
+ # pyproject version, so git tags track the PyPI release 1:1 (PyPI itself is
5
+ # published by publish.yml on the same push). The storage-stack OCI artifact
6
+ # (docker-compose.yml -> ghcr.io/<owner>/data-refinery-stack) is republished
7
+ # ONLY when docker-compose.yml actually changed since the previous tag — most
8
+ # PRs touch CLI code, not the substrate, so the image would otherwise be
9
+ # byte-identical churn.
10
+ #
11
+ # Manual (re)publish of a specific version: run via workflow_dispatch with the
12
+ # tag input; that always publishes the stack for that tag.
13
+ #
14
+ # When the stack IS published, two consumption paths ship for the version:
15
+ # 1. OCI via `docker compose publish` —
16
+ # ghcr.io/<owner>/data-refinery-stack:<version> (+ :latest). A single
17
+ # manifest REFERENCING upstream mongo:8.0 + neo4j:5-community (not a custom
18
+ # image). Consume with:
19
+ # docker compose -f oci://ghcr.io/<owner>/data-refinery-stack:<version> up -d
20
+ # 2. The raw docker-compose.yml attached to the GitHub Release.
21
+ #
22
+ # Tags/releases are immutable per version: a given <version> is published once.
23
+
24
+ on:
25
+ push:
26
+ branches: [main]
27
+ workflow_dispatch:
28
+ inputs:
29
+ tag:
30
+ description: "Tag to (re)publish the stack for, e.g. v0.5.0"
31
+ required: true
32
+
33
+ jobs:
34
+ tag:
35
+ runs-on: ubuntu-latest
36
+ permissions:
37
+ contents: write # push the version tag
38
+ outputs:
39
+ tag: ${{ steps.plan.outputs.tag }}
40
+ version: ${{ steps.plan.outputs.version }}
41
+ publish: ${{ steps.plan.outputs.publish }}
42
+ steps:
43
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
44
+ with:
45
+ fetch-depth: 0
46
+ fetch-tags: true
47
+
48
+ - name: Plan tag + compose-change gate
49
+ id: plan
50
+ env:
51
+ EVENT_NAME: ${{ github.event_name }}
52
+ INPUT_TAG: ${{ github.event.inputs.tag }}
53
+ run: |
54
+ set -euo pipefail
55
+
56
+ if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
57
+ # Manual: (re)publish exactly the requested tag, always.
58
+ TAG="$INPUT_TAG"
59
+ VERSION="${TAG#v}"
60
+ PUBLISH="true"
61
+ echo "Manual dispatch: will publish stack for ${TAG}"
62
+ else
63
+ # Push to main: derive the version from pyproject and tag it.
64
+ VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml | head -n1)"
65
+ if [ -z "$VERSION" ]; then
66
+ echo "::error::could not read version from pyproject.toml"
67
+ exit 1
68
+ fi
69
+ TAG="v${VERSION}"
70
+ # Previous tag = highest v* EXCLUDING the current one. Excluding TAG
71
+ # matters on a re-run: if an earlier run pushed the tag but then
72
+ # failed before publishing, we must still compare against the real
73
+ # previous release and be able to re-publish to recover — never treat
74
+ # "tag exists" as "already published" and skip. Tag CREATION below is
75
+ # still idempotent (skipped when the tag exists); only publishing is
76
+ # decided by the compose-change gate.
77
+ PREV="$(git tag --list 'v*' --sort=-v:refname | grep -vxF "${TAG}" | head -n1 || true)"
78
+ if [ -z "$PREV" ] || ! git diff --quiet "${PREV}" HEAD -- docker-compose.yml; then
79
+ PUBLISH="true"
80
+ echo "Substrate changed (or bootstrap) since ${PREV:-none} — will publish stack."
81
+ else
82
+ PUBLISH="false"
83
+ echo "docker-compose.yml unchanged since ${PREV} — tag only, no stack publish."
84
+ fi
85
+ fi
86
+
87
+ # Create the tag if it does not exist yet (push: always new here;
88
+ # dispatch: only when targeting a not-yet-created tag).
89
+ if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
90
+ git config user.name "github-actions[bot]"
91
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
92
+ git tag -a "${TAG}" -m "data-refinery-cli ${TAG}"
93
+ git push origin "${TAG}"
94
+ echo "Created and pushed ${TAG}"
95
+ fi
96
+
97
+ {
98
+ echo "tag=${TAG}"
99
+ echo "version=${VERSION}"
100
+ echo "publish=${PUBLISH}"
101
+ } >> "$GITHUB_OUTPUT"
102
+
103
+ publish-stack:
104
+ needs: tag
105
+ if: needs.tag.outputs.publish == 'true'
106
+ runs-on: ubuntu-latest
107
+ permissions:
108
+ contents: write # create/attach the release asset
109
+ packages: write # push the OCI artifact to GHCR
110
+ steps:
111
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
112
+ with:
113
+ ref: ${{ needs.tag.outputs.tag }}
114
+
115
+ - name: Resolve image ref
116
+ id: ref
117
+ run: |
118
+ IMAGE="ghcr.io/${GITHUB_REPOSITORY_OWNER}/data-refinery-stack"
119
+ IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
120
+ echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
121
+ echo "Publishing ${IMAGE}:${{ needs.tag.outputs.version }}"
122
+
123
+ - name: Verify compose + OCI publish support
124
+ run: |
125
+ docker compose version
126
+ docker compose -f docker-compose.yml config --quiet
127
+ # `docker compose publish` landed in Compose v2.24 — fail early with a
128
+ # clear message on an older runner instead of a cryptic publish error.
129
+ docker compose publish --help >/dev/null 2>&1 || {
130
+ echo "::error::this docker compose has no 'publish' subcommand (needs v2.24+)"
131
+ exit 1
132
+ }
133
+ echo "compose validates; publish supported"
134
+
135
+ - name: Log in to GHCR
136
+ run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
137
+
138
+ - name: Publish compose as an OCI artifact
139
+ env:
140
+ IMAGE: ${{ steps.ref.outputs.image }}
141
+ VERSION: ${{ needs.tag.outputs.version }}
142
+ run: |
143
+ docker compose -f docker-compose.yml publish "${IMAGE}:${VERSION}" -y
144
+ docker compose -f docker-compose.yml publish "${IMAGE}:latest" -y
145
+
146
+ - name: Attach docker-compose.yml to the release (portable fallback)
147
+ env:
148
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
149
+ RAW_TAG: ${{ needs.tag.outputs.tag }}
150
+ IMAGE: ${{ steps.ref.outputs.image }}
151
+ VERSION: ${{ needs.tag.outputs.version }}
152
+ run: |
153
+ if gh release view "${RAW_TAG}" >/dev/null 2>&1; then
154
+ gh release upload "${RAW_TAG}" docker-compose.yml --clobber
155
+ else
156
+ # --target pins the release (and the tag) at this commit, so a manual
157
+ # dispatch for a not-yet-existing tag still works.
158
+ gh release create "${RAW_TAG}" docker-compose.yml \
159
+ --target "${GITHUB_SHA}" \
160
+ --title "${RAW_TAG}" \
161
+ --notes "Storage stack (mongo 27018 + neo4j 7687). OCI: docker compose -f oci://${IMAGE}:${VERSION} up -d"
162
+ fi
163
+
164
+ - name: Summary
165
+ env:
166
+ IMAGE: ${{ steps.ref.outputs.image }}
167
+ VERSION: ${{ needs.tag.outputs.version }}
168
+ RAW_TAG: ${{ needs.tag.outputs.tag }}
169
+ run: |
170
+ {
171
+ echo "### Storage stack published"
172
+ echo ""
173
+ echo "- OCI: \`docker compose -f oci://${IMAGE}:${VERSION} up -d\`"
174
+ echo "- Release asset: \`docker-compose.yml\` on release ${RAW_TAG}"
175
+ } >> "$GITHUB_STEP_SUMMARY"
@@ -31,11 +31,23 @@ jobs:
31
31
 
32
32
  - run: uv run pytest -n auto --cov=data_refinery --cov-report=xml:coverage.xml --cov-report=term -v
33
33
 
34
+ # Stamp each analysis with the package version so SonarCloud's "Previous
35
+ # version" New Code period has a real boundary to diff against (the version
36
+ # is bumped every PR; see version-check below + the version-bump skill).
37
+ - name: Resolve project version
38
+ id: ver
39
+ run: |
40
+ VERSION=$(uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
41
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
42
+
34
43
  - name: SonarCloud Scan
35
44
  if: env.SONAR_TOKEN != ''
36
45
  uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6
37
46
  env:
38
47
  SONAR_HOST_URL: https://sonarcloud.io
48
+ with:
49
+ args: >
50
+ -Dsonar.projectVersion=${{ steps.ver.outputs.version }}
39
51
 
40
52
  lint:
41
53
  runs-on: ubuntu-latest
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). This project
6
6
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.2] - 2026-06-21
9
+
10
+ ### Changed
11
+
12
+ - `publish-stack.yml` now runs on every push to `main`: a `tag` job derives the version from `pyproject.toml` and creates the `v<version>` git tag (tags now track the PyPI release 1:1), and the stack OCI image is **republished only when `docker-compose.yml` changed** since the previous tag — most releases bump the CLI, not the substrate, so the image no longer churns on every merge. A `workflow_dispatch` with a `tag` input still force-(re)publishes a specific version. (`docs/stack-image.md` + `CLAUDE.md` updated.)
13
+
14
+ ## [0.5.1] - 2026-06-21
15
+
16
+ ### Changed
17
+
18
+ - CI stamps each SonarCloud analysis with `sonar.projectVersion` read from `pyproject.toml`, so the "Previous version" New Code period has a real per-release boundary to diff against (`.github/workflows/tests.yml`)
19
+
20
+ ### Fixed
21
+
22
+ - Cleared 4 pre-existing SonarCloud smells in `data_refinery/cli/_commands/stack.py` (wave-1), behaviour-preserving: merged the implicitly concatenated `_DOCKER_HINT` literal (S5799), extracted `_load_ps_rows` to drop `_parse_ps` cognitive complexity (S3776), single-exit `cmd_stack_status` via a `_render_status_text` helper (S3516), and an `_add_json_flag` helper for the repeated `"Emit structured JSON."` literal (S1192)
23
+
8
24
  ## [0.5.0] - 2026-06-20
9
25
 
10
26
  ### Added
@@ -14,7 +14,9 @@ and freshness of data as it is stored and fetched. It is being split out of
14
14
  *Wave 1* (issue #1): the storage substrate (`docker-compose.yml` — mongo 27018 +
15
15
  neo4j 7687/apoc) and the `data-refinery stack up/down/status` verb wrapping
16
16
  `docker compose`, plus the GHCR publish workflow
17
- (`.github/workflows/publish-stack.yml`) and the pinnable docs
17
+ (`.github/workflows/publish-stack.yml` on every push to `main` it auto-tags
18
+ `v<version>` from `pyproject.toml`, then republishes the stack OCI image **only
19
+ when `docker-compose.yml` changed** since the previous tag) and the pinnable docs
18
20
  (`docs/stack-image.md`, `docs/contract.md`). *Wave 2* (issue #3): the
19
21
  storage-neutral **store** — the generic envelope (`data_refinery/store/`), the
20
22
  importable `data_refinery.store.put/get/list` library mirrored by `data-refinery
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-refinery-cli
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Agent and CLI for data quality in storage and retrieval — validating, deduplicating, and checking the integrity and freshness of data as it is stored and fetched. Split out of eidetic-cli so eidetic keeps agent-memory; sibling to daria, the Data Refinery Intelligent Agent.
5
5
  Project-URL: Homepage, https://github.com/agentculture/data-refinery-cli
6
6
  Project-URL: Issues, https://github.com/agentculture/data-refinery-cli/issues
@@ -27,13 +27,14 @@ import json
27
27
  import shutil
28
28
  import subprocess # nosec B404 - used with a fixed argv, never shell=True
29
29
  from pathlib import Path
30
+ from typing import Any
30
31
 
31
32
  from data_refinery.cli._errors import EXIT_ENV_ERROR, CliError
32
33
  from data_refinery.cli._output import emit_diagnostic, emit_result
33
34
 
34
35
  _COMPOSE_FILENAME = "docker-compose.yml"
35
36
  _DOCKER_HINT = (
36
- "install Docker and ensure 'docker compose' works " "(https://docs.docker.com/get-docker/)"
37
+ "install Docker and ensure 'docker compose' works (https://docs.docker.com/get-docker/)"
37
38
  )
38
39
  _COMPOSE_HINT = (
39
40
  "run from the data-refinery-cli repo (it ships docker-compose.yml), or pull "
@@ -113,26 +114,37 @@ def _compose(compose: Path, *args: str) -> subprocess.CompletedProcess[str]:
113
114
  return proc
114
115
 
115
116
 
117
+ def _load_ps_rows(text: str) -> list[Any]:
118
+ """Decode compose ps output: a top-level JSON array, else one object per line.
119
+
120
+ Split out of :func:`_parse_ps` so the two-mode decoding (with its nested
121
+ error handling) doesn't push that function's cognitive complexity over the
122
+ limit. *text* is assumed already stripped and non-empty.
123
+ """
124
+ try:
125
+ parsed = json.loads(text)
126
+ return parsed if isinstance(parsed, list) else [parsed]
127
+ except json.JSONDecodeError:
128
+ pass
129
+ rows: list[Any] = []
130
+ for line in text.splitlines():
131
+ line = line.strip()
132
+ if not line:
133
+ continue
134
+ try:
135
+ rows.append(json.loads(line))
136
+ except json.JSONDecodeError:
137
+ continue
138
+ return rows
139
+
140
+
116
141
  def _parse_ps(stdout: str) -> list[dict[str, object]]:
117
142
  """Parse ``docker compose ps --format json`` (array OR one-object-per-line)."""
118
143
  text = stdout.strip()
119
144
  if not text:
120
145
  return []
121
- try:
122
- parsed = json.loads(text)
123
- rows = parsed if isinstance(parsed, list) else [parsed]
124
- except json.JSONDecodeError:
125
- rows = []
126
- for line in text.splitlines():
127
- line = line.strip()
128
- if not line:
129
- continue
130
- try:
131
- rows.append(json.loads(line))
132
- except json.JSONDecodeError:
133
- continue
134
146
  services: list[dict[str, object]] = []
135
- for row in rows:
147
+ for row in _load_ps_rows(text):
136
148
  if not isinstance(row, dict):
137
149
  continue
138
150
  services.append(
@@ -198,6 +210,18 @@ def cmd_stack_down(args: argparse.Namespace) -> int:
198
210
  return 0
199
211
 
200
212
 
213
+ def _render_status_text(payload: dict[str, object]) -> str:
214
+ """Render the human-readable ``stack status`` text from a status payload."""
215
+ services = payload["services"]
216
+ if not services:
217
+ return "stack status: no services running (try 'data-refinery stack up')"
218
+ lines = [f"compose: {payload['compose_file']}", f"healthy: {payload['healthy']}"]
219
+ for s in services: # type: ignore[attr-defined]
220
+ health = f" ({s['health']})" if s["health"] else ""
221
+ lines.append(f"- {s['name']}: {s['state']}{health}")
222
+ return "\n".join(lines)
223
+
224
+
201
225
  def cmd_stack_status(args: argparse.Namespace) -> int:
202
226
  _require_docker()
203
227
  compose = _require_compose()
@@ -206,18 +230,8 @@ def cmd_stack_status(args: argparse.Namespace) -> int:
206
230
  payload["command"] = "status"
207
231
  if json_mode:
208
232
  emit_result(payload, json_mode=True)
209
- return 0
210
- services = payload["services"]
211
- if not services:
212
- emit_result(
213
- "stack status: no services running (try 'data-refinery stack up')", json_mode=False
214
- )
215
- return 0
216
- lines = [f"compose: {payload['compose_file']}", f"healthy: {payload['healthy']}"]
217
- for s in services:
218
- health = f" ({s['health']})" if s["health"] else ""
219
- lines.append(f"- {s['name']}: {s['state']}{health}")
220
- emit_result("\n".join(lines), json_mode=False)
233
+ else:
234
+ emit_result(_render_status_text(payload), json_mode=False)
221
235
  return 0
222
236
 
223
237
 
@@ -254,28 +268,32 @@ def _stack_overview(args: argparse.Namespace) -> int:
254
268
  return 0
255
269
 
256
270
 
271
+ def _add_json_flag(p: argparse.ArgumentParser) -> None:
272
+ p.add_argument("--json", action="store_true", help="Emit structured JSON.")
273
+
274
+
257
275
  def register(sub: argparse._SubParsersAction) -> None:
258
276
  p = sub.add_parser(
259
277
  "stack",
260
278
  help="Manage the storage substrate (mongo + neo4j) via docker compose.",
261
279
  )
262
- p.add_argument("--json", action="store_true", help="Emit structured JSON.")
280
+ _add_json_flag(p)
263
281
  p.set_defaults(func=_stack_overview, json=False)
264
282
  # Propagate the structured-error parser_class to nested verbs.
265
283
  verb = p.add_subparsers(dest="stack_command", parser_class=type(p))
266
284
 
267
285
  up = verb.add_parser("up", help="Bring the stack up (docker compose up -d).")
268
- up.add_argument("--json", action="store_true", help="Emit structured JSON.")
286
+ _add_json_flag(up)
269
287
  up.set_defaults(func=cmd_stack_up)
270
288
 
271
289
  down = verb.add_parser("down", help="Stop the stack (docker compose down).")
272
- down.add_argument("--json", action="store_true", help="Emit structured JSON.")
290
+ _add_json_flag(down)
273
291
  down.set_defaults(func=cmd_stack_down)
274
292
 
275
293
  status = verb.add_parser("status", help="Report per-service state + health.")
276
- status.add_argument("--json", action="store_true", help="Emit structured JSON.")
294
+ _add_json_flag(status)
277
295
  status.set_defaults(func=cmd_stack_status)
278
296
 
279
297
  ov = verb.add_parser("overview", help="Describe the stack noun.")
280
- ov.add_argument("--json", action="store_true", help="Emit structured JSON.")
298
+ _add_json_flag(ov)
281
299
  ov.set_defaults(func=_stack_overview)
@@ -33,8 +33,13 @@ project.
33
33
  - **Name:** `ghcr.io/agentculture/data-refinery-stack`
34
34
  - **Tags:** the release version without the leading `v` (e.g. `0.4.0`), plus
35
35
  `latest`.
36
- - **Immutability:** a given version tag is published exactly once, on the
37
- `v<version>` git tag. Treat `0.4.0` as immutable; `latest` floats.
36
+ - **Immutability:** a given version tag is published exactly once. Treat
37
+ `0.4.0` as immutable; `latest` floats.
38
+ - **Cadence:** the image is **only (re)published when `docker-compose.yml`
39
+ actually changes** between releases — most releases bump the CLI, not the
40
+ substrate, so the stack image stays put while the PyPI version moves. The
41
+ image version therefore tracks the latest release in which the compose
42
+ changed, not every release.
38
43
 
39
44
  > **Not a multi-arch image.** The published artifact is a single OCI manifest
40
45
  > produced by `docker compose publish` — it *references* the upstream `mongo:8.0`
@@ -81,6 +86,11 @@ consumer that needs byte-for-byte reproducibility should pin the upstream image
81
86
  ## Publishing
82
87
 
83
88
  [`.github/workflows/publish-stack.yml`](../.github/workflows/publish-stack.yml)
84
- runs on a `v*` tag (or `workflow_dispatch`): it validates the compose, logs in to
85
- GHCR with the workflow token, `docker compose publish`es the OCI artifact at the
86
- version tag and `latest`, and attaches `docker-compose.yml` to the release.
89
+ runs on every **push to `main`** (a merged PR). Its `tag` job derives the version
90
+ from `pyproject.toml` and creates the `v<version>` git tag (so tags track the
91
+ PyPI release 1:1), then gates: it only proceeds to publish when `docker-compose.yml`
92
+ differs from the previous tag. When it does, the `publish-stack` job validates the
93
+ compose, logs in to GHCR with the workflow token, `docker compose publish`es the
94
+ OCI artifact at the version tag and `latest`, and attaches `docker-compose.yml` to
95
+ the release. A `workflow_dispatch` with a `tag` input force-(re)publishes a
96
+ specific version's stack regardless of the gate.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "data-refinery-cli"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "Agent and CLI for data quality in storage and retrieval — validating, deduplicating, and checking the integrity and freshness of data as it is stored and fetched. Split out of eidetic-cli so eidetic keeps agent-memory; sibling to daria, the Data Refinery Intelligent Agent."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -156,7 +156,7 @@ wheels = [
156
156
 
157
157
  [[package]]
158
158
  name = "data-refinery-cli"
159
- version = "0.5.0"
159
+ version = "0.5.2"
160
160
  source = { editable = "." }
161
161
 
162
162
  [package.optional-dependencies]
@@ -1,95 +0,0 @@
1
- name: Publish storage stack to GHCR
2
-
3
- # Publishes the storage substrate (docker-compose.yml) on a version tag.
4
- #
5
- # Two consumption paths are published for the same version (issue #1):
6
- # 1. An OCI artifact via `docker compose publish` —
7
- # ghcr.io/agentculture/data-refinery-stack:<version> (+ :latest).
8
- # The artifact is a SINGLE manifest that REFERENCES the upstream
9
- # mongo:8.0 + neo4j:5-community images (which are themselves multi-arch);
10
- # it is not a custom multi-arch image. Consume with:
11
- # docker compose -f oci://ghcr.io/agentculture/data-refinery-stack:<version> up -d
12
- # 2. The raw docker-compose.yml attached to the GitHub Release, for consumers
13
- # whose docker/compose predates OCI compose support — curl + `-f` it.
14
- #
15
- # Tags are immutable per release: a given <version> is published once.
16
-
17
- on:
18
- push:
19
- tags:
20
- - "v*"
21
- workflow_dispatch:
22
- inputs:
23
- tag:
24
- description: "Tag to publish (e.g. v0.4.0)"
25
- required: true
26
-
27
- permissions:
28
- contents: write # create/attach the release asset
29
- packages: write # push the OCI artifact to GHCR
30
-
31
- jobs:
32
- publish-stack:
33
- runs-on: ubuntu-latest
34
- steps:
35
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
36
-
37
- - name: Resolve version + image ref
38
- id: ref
39
- run: |
40
- RAW_TAG="${{ github.event.inputs.tag || github.ref_name }}"
41
- VERSION="${RAW_TAG#v}"
42
- IMAGE="ghcr.io/${GITHUB_REPOSITORY_OWNER}/data-refinery-stack"
43
- IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
44
- echo "raw_tag=${RAW_TAG}" >> "$GITHUB_OUTPUT"
45
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
46
- echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
47
- echo "Publishing ${IMAGE}:${VERSION}"
48
-
49
- - name: Verify compose + OCI publish support
50
- run: |
51
- docker compose version
52
- docker compose -f docker-compose.yml config --quiet
53
- # `docker compose publish` landed in Compose v2.24 — fail early with a
54
- # clear message on an older runner instead of a cryptic publish error.
55
- docker compose publish --help >/dev/null 2>&1 || {
56
- echo "::error::this docker compose has no 'publish' subcommand (needs v2.24+)"
57
- exit 1
58
- }
59
- echo "compose validates; publish supported"
60
-
61
- - name: Log in to GHCR
62
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
63
-
64
- - name: Publish compose as an OCI artifact
65
- env:
66
- IMAGE: ${{ steps.ref.outputs.image }}
67
- VERSION: ${{ steps.ref.outputs.version }}
68
- run: |
69
- docker compose -f docker-compose.yml publish "${IMAGE}:${VERSION}" -y
70
- docker compose -f docker-compose.yml publish "${IMAGE}:latest" -y
71
-
72
- - name: Attach docker-compose.yml to the release (portable fallback)
73
- env:
74
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75
- RAW_TAG: ${{ steps.ref.outputs.raw_tag }}
76
- run: |
77
- if gh release view "${RAW_TAG}" >/dev/null 2>&1; then
78
- gh release upload "${RAW_TAG}" docker-compose.yml --clobber
79
- else
80
- # --target pins the release (and creates the tag) at this commit, so
81
- # a manual workflow_dispatch with a not-yet-existing tag still works.
82
- gh release create "${RAW_TAG}" docker-compose.yml \
83
- --target "${GITHUB_SHA}" \
84
- --title "${RAW_TAG}" \
85
- --notes "Storage stack (mongo 27018 + neo4j 7687). OCI: docker compose -f oci://${{ steps.ref.outputs.image }}:${{ steps.ref.outputs.version }} up -d"
86
- fi
87
-
88
- - name: Summary
89
- run: |
90
- {
91
- echo "### Storage stack published"
92
- echo ""
93
- echo "- OCI: \`docker compose -f oci://${{ steps.ref.outputs.image }}:${{ steps.ref.outputs.version }} up -d\`"
94
- echo "- Release asset: \`docker-compose.yml\` on release ${{ steps.ref.outputs.raw_tag }}"
95
- } >> "$GITHUB_STEP_SUMMARY"