Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87d46226f6 | ||
|
|
4638e334b5 | ||
|
|
c6bd4e8f63 | ||
|
|
d47e3833bc | ||
|
|
c00e9e3d7b | ||
|
|
40e95bc96f | ||
|
|
3aa9681404 | ||
|
|
e96f63cb40 | ||
|
|
6d6c8cceec | ||
|
|
d9489b5387 | ||
|
|
35f7bc92cd | ||
|
|
55bcd8fa01 | ||
|
|
990f70845c | ||
|
|
149e8846b8 | ||
|
|
a7cdebc0dc | ||
|
|
012f0979ce | ||
|
|
62412755cb | ||
|
|
25f00335df |
@@ -0,0 +1,135 @@
|
|||||||
|
# dependency-graph
|
||||||
|
|
||||||
|
Map of the oblachno ecosystem. Knows which repo produces what, which
|
||||||
|
repos depend on which, and the correct order for cross-repo changes.
|
||||||
|
|
||||||
|
## When to Invoke
|
||||||
|
|
||||||
|
Invoke this skill when:
|
||||||
|
- Changes span multiple repos
|
||||||
|
- A change in one repo requires version bumps in downstream repos
|
||||||
|
- Deploying infrastructure that depends on published packages/images
|
||||||
|
- Verifying the ecosystem is in a consistent state before deployment
|
||||||
|
- Determining which repos to update and in what order
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- All repos cloned under `/home/emo/dev/ideas/oblachno/`
|
||||||
|
- `.env` with `DEVELOPER_GITEA_API_TOKEN` in each repo
|
||||||
|
|
||||||
|
## Ecosystem Map
|
||||||
|
|
||||||
|
```
|
||||||
|
devx (PyPI package)
|
||||||
|
/ | \
|
||||||
|
/ | \
|
||||||
|
grm sso-bridge infra
|
||||||
|
(PyPI) (PyPI+Docker) (deploys all)
|
||||||
|
| | |
|
||||||
|
v v v
|
||||||
|
infra bump infra bump staging
|
||||||
|
(auto PR) (auto PR) production
|
||||||
|
|
|
||||||
|
mattermost-oidc (Docker image)
|
||||||
|
(infra pulls :latest at deploy)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repositories
|
||||||
|
|
||||||
|
| Repo | Produces | Consumers | Release Trigger |
|
||||||
|
|------|----------|-----------|-----------------|
|
||||||
|
| `devx` | PyPI package `devx` | grm, sso-bridge, infra | User-facing changes to `src/devx/**` |
|
||||||
|
| `grm` | PyPI package `grm` | infra | User-facing changes to `src/grm/**` or `ansible/**` |
|
||||||
|
| `sso-bridge` | PyPI package `sso_bridge` + Docker image | infra | User-facing changes to `src/sso_bridge/**` or `ansible/**` |
|
||||||
|
| `infra` | Staging/production deployment | (end users) | User-facing changes + nightly gate |
|
||||||
|
| `mattermost-oidc` | Docker image `mattermost-oidc` | infra (pulls at deploy) | `Dockerfile` or `build.yml` changes |
|
||||||
|
|
||||||
|
## Dependency Chain
|
||||||
|
|
||||||
|
### devx → all repos
|
||||||
|
|
||||||
|
devx publishes to the Gitea PyPI registry. grm, sso-bridge, and infra
|
||||||
|
pin devx in `pyproject.toml`:
|
||||||
|
```toml
|
||||||
|
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@vX.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
When devx publishes a new version:
|
||||||
|
1. grm, sso-bridge, and infra must bump their pinned devx version
|
||||||
|
2. This is currently manual — no auto-dependency-PR from devx
|
||||||
|
3. Each repo must `make setup` to pick up the new version
|
||||||
|
|
||||||
|
### grm → infra
|
||||||
|
|
||||||
|
grm publishes to PyPI. Its post-merge workflow auto-creates an infra
|
||||||
|
dependency PR via `devx.ci.create_dependency_pr --repo oblachno/infra
|
||||||
|
--package grm`. The PR bumps the pinned grm version in infra's
|
||||||
|
`pyproject.toml`.
|
||||||
|
|
||||||
|
### sso-bridge → infra
|
||||||
|
|
||||||
|
sso-bridge publishes to PyPI AND builds a Docker image. Its post-merge
|
||||||
|
workflow auto-creates an infra dependency PR via
|
||||||
|
`devx.ci.create_dependency_pr --repo oblachno/infra --package
|
||||||
|
sso_bridge`. The PR bumps the pinned sso_bridge version.
|
||||||
|
|
||||||
|
The Docker image is pulled by infra at deploy time (`sso-bridge:latest`).
|
||||||
|
|
||||||
|
### mattermost-oidc → infra
|
||||||
|
|
||||||
|
mattermost-oidc builds a Docker image tagged `:latest` and `:MM_VERSION`.
|
||||||
|
infra pulls `mattermost-oidc:latest` at deploy time. There is no
|
||||||
|
auto-dependency-PR — infra simply pulls the latest image.
|
||||||
|
|
||||||
|
### infra → staging/production
|
||||||
|
|
||||||
|
infra deploys to staging and production. The deployment:
|
||||||
|
1. Provisions VMs from golden images
|
||||||
|
2. Runs Ansible roles (including grm and sso-bridge roles)
|
||||||
|
3. Pulls Docker images (sso-bridge, mattermost-oidc)
|
||||||
|
4. Configures services
|
||||||
|
|
||||||
|
## Correct Order for Cross-Repo Changes
|
||||||
|
|
||||||
|
When a change spans multiple repos, follow this order:
|
||||||
|
|
||||||
|
1. **devx first** — if the change starts in devx, merge and publish devx
|
||||||
|
first. Wait for the PyPI publish job to complete.
|
||||||
|
2. **Bump devx in consumers** — in grm/sso-bridge/infra, bump the pinned
|
||||||
|
devx version, run `make setup`, verify tests pass, merge.
|
||||||
|
3. **grm/sso-bridge second** — merge and publish grm/sso-bridge. Wait
|
||||||
|
for the PyPI publish + Docker image build to complete.
|
||||||
|
4. **Auto-dependency-PRs** — grm/sso-bridge post-merge auto-creates infra
|
||||||
|
PRs to bump pinned versions. Wait for these PRs to appear.
|
||||||
|
5. **Merge infra dependency PRs** — review and merge the auto-created
|
||||||
|
infra PRs.
|
||||||
|
6. **infra last** — deploy to staging, validate, promote to production.
|
||||||
|
|
||||||
|
## State Verification Before Deployment
|
||||||
|
|
||||||
|
Before deploying infra, verify:
|
||||||
|
|
||||||
|
1. **devx version consistent** — all repos pin the same devx version
|
||||||
|
2. **grm published** — latest grm tag exists in PyPI
|
||||||
|
3. **sso-bridge published** — latest sso_bridge tag exists in PyPI
|
||||||
|
4. **sso-bridge image built** — latest sso-bridge Docker image exists
|
||||||
|
5. **mattermost-oidc image built** — latest mattermost-oidc image exists
|
||||||
|
6. **infra pins match published versions** — no stale pins
|
||||||
|
7. **Nightly gate green** — `NIGHTLY_STATUS` is not `failed`
|
||||||
|
|
||||||
|
## Quick Check Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check latest devx version
|
||||||
|
curl -sS https://git.oblachno.oblachno.fyi/api/v1/repos/oblachno-oss/devx/releases/latest | python3 -c "import json,sys; print(json.load(sys.stdin).get('tag_name','?'))"
|
||||||
|
|
||||||
|
# Check pinned devx version in each repo
|
||||||
|
for repo in grm sso-bridge infra; do
|
||||||
|
echo -n "$repo: "; grep 'devx @' /home/emo/dev/ideas/oblachno/$repo/pyproject.toml | grep -oP 'v[\d.]+'
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check latest sso-bridge image build
|
||||||
|
curl -sS -H "Authorization: token $DEVELOPER_GITEA_API_TOKEN" \
|
||||||
|
"https://git.oblachno.oblachno.fyi/api/v1/repos/oblachno/sso-bridge/actions/runs?per_page=5" \
|
||||||
|
| python3 -c "import json,sys; [print(r['id'],r['status'],r['conclusion']) for r in json.load(sys.stdin).get('workflow_runs',[]) if r.get('event')=='push']"
|
||||||
|
```
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# deployment-coordination
|
||||||
|
|
||||||
|
How devx releases propagate to downstream repos. devx is the base
|
||||||
|
package — all other repos pin it. A devx release must complete before
|
||||||
|
consumers can bump.
|
||||||
|
|
||||||
|
## When to Invoke
|
||||||
|
|
||||||
|
Invoke this skill when:
|
||||||
|
- Changes to devx affect downstream repos (grm, sso-bridge, infra)
|
||||||
|
- Preparing a devx release that other repos depend on
|
||||||
|
- Verifying downstream repos have bumped to the latest devx version
|
||||||
|
- Coordinating a multi-repo change that starts in devx
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- devx repo at `/home/emo/dev/ideas/oblachno/devx`
|
||||||
|
- `.env` with `DEVELOPER_GITEA_API_TOKEN`
|
||||||
|
- See `dependency-graph` skill for the full ecosystem map
|
||||||
|
|
||||||
|
## What devx Produces
|
||||||
|
|
||||||
|
devx publishes a Python package to the Gitea PyPI registry. Downstream
|
||||||
|
repos pin it:
|
||||||
|
```toml
|
||||||
|
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@vX.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Flow
|
||||||
|
|
||||||
|
1. PR merged to master
|
||||||
|
2. Post-merge workflow runs `devx.ci.release` — classifies changes
|
||||||
|
3. If user-facing changes: git-cliff bumps version, creates tag, pushes
|
||||||
|
4. `devx.ci.publish` builds and publishes to Gitea PyPI registry
|
||||||
|
5. Downstream repos must bump their pinned devx version
|
||||||
|
|
||||||
|
## Downstream Consumers
|
||||||
|
|
||||||
|
| Repo | Pin location | Auto-bump? |
|
||||||
|
|------|-------------|------------|
|
||||||
|
| grm | `pyproject.toml` | No — manual |
|
||||||
|
| sso-bridge | `pyproject.toml` | No — manual |
|
||||||
|
| infra | `pyproject.toml` | No — manual |
|
||||||
|
|
||||||
|
devx does NOT auto-create dependency PRs in downstream repos. Bumping
|
||||||
|
is manual: create a PR in each downstream repo to update the pinned
|
||||||
|
version.
|
||||||
|
|
||||||
|
## Coordinating a devx Change
|
||||||
|
|
||||||
|
When a change to devx affects downstream repos:
|
||||||
|
|
||||||
|
1. **Merge devx PR** — wait for post-merge publish to complete
|
||||||
|
2. **Verify publish** — check the new tag exists:
|
||||||
|
```bash
|
||||||
|
curl -sS -H "Authorization: token $DEVELOPER_GITEA_API_TOKEN" \
|
||||||
|
https://git.oblachno.oblachno.fyi/api/v1/repos/oblachno-oss/devx/releases/latest \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('tag_name','?'))"
|
||||||
|
```
|
||||||
|
3. **Bump downstream repos** — for each repo (grm, sso-bridge, infra):
|
||||||
|
- Update `devx @ ...@vX.Y.Z` in `pyproject.toml`
|
||||||
|
- Run `make setup` to install the new version
|
||||||
|
- Run `make pytest-cov` to verify compatibility
|
||||||
|
- Create and merge a PR
|
||||||
|
4. **Verify infra deploys** — after infra bumps, verify staging deploy
|
||||||
|
picks up the new devx version
|
||||||
|
|
||||||
|
## State Verification
|
||||||
|
|
||||||
|
Before starting a devx change, verify current state:
|
||||||
|
```bash
|
||||||
|
# Current devx version
|
||||||
|
grep '__version__' /home/emo/dev/ideas/oblachno/devx/src/devx/__init__.py
|
||||||
|
|
||||||
|
# What each repo pins
|
||||||
|
for repo in grm sso-bridge infra; do
|
||||||
|
echo -n "$repo pins: "
|
||||||
|
grep 'devx @' /home/emo/dev/ideas/oblachno/$repo/pyproject.toml | grep -oP 'v[\d.]+'
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
If pins are inconsistent across repos, bump them to the latest published
|
||||||
|
version before starting new work.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
- Merging a devx PR and immediately merging downstream PRs without
|
||||||
|
waiting for the publish job to complete
|
||||||
|
- Forgetting to bump infra (it has the most complex deploy pipeline)
|
||||||
|
- Bumping only one downstream repo when the change affects all three
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# skill-creation
|
||||||
|
|
||||||
|
How to create, validate, and maintain Devin skills. Skills must be
|
||||||
|
clear, succinct, and actionable — no AI slop.
|
||||||
|
|
||||||
|
## When to Invoke
|
||||||
|
|
||||||
|
Invoke this skill when:
|
||||||
|
- Creating a new skill
|
||||||
|
- Amending an existing skill
|
||||||
|
- Evaluating whether a skill is needed
|
||||||
|
- Reviewing a PR that adds or modifies skills
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Skill directory: `.devin/skills/<skill-name>/SKILL.md`
|
||||||
|
- Validator: `tests/test_skills.py` (infra) or reference to it
|
||||||
|
- Tests: `tests/unit/test_skills_validation.py` (infra)
|
||||||
|
|
||||||
|
## When to Create a Skill
|
||||||
|
|
||||||
|
Create a skill when:
|
||||||
|
- An agent struggles with a task repeatedly (branch hygiene, PR order)
|
||||||
|
- A workflow has non-obvious ordering constraints (deployment coordination)
|
||||||
|
- A task requires specific tool usage over raw commands (CI monitoring)
|
||||||
|
- Multiple agents need shared context (dependency graph)
|
||||||
|
|
||||||
|
Do NOT create a skill for:
|
||||||
|
- One-off tasks (use a spec instead)
|
||||||
|
- Tasks already covered by AGENTS.md
|
||||||
|
- Tasks that are obvious from the Makefile or README
|
||||||
|
- Tasks that change frequently (skills should be stable)
|
||||||
|
|
||||||
|
## Skill Structure
|
||||||
|
|
||||||
|
Every skill MUST have:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# <skill-name>
|
||||||
|
|
||||||
|
One-line description of what the skill does.
|
||||||
|
|
||||||
|
## When to Invoke
|
||||||
|
|
||||||
|
2-4 bullet points describing when to use this skill.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
What must exist before using the skill (venv, .env, tools).
|
||||||
|
|
||||||
|
## <Core Content>
|
||||||
|
|
||||||
|
The actual guidance. Keep it actionable.
|
||||||
|
|
||||||
|
## Verification (if applicable)
|
||||||
|
|
||||||
|
How to verify the skill's guidance works.
|
||||||
|
|
||||||
|
## Common Mistakes (if applicable)
|
||||||
|
|
||||||
|
What agents get wrong without this skill.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality Standards
|
||||||
|
|
||||||
|
### Do
|
||||||
|
- **Be specific.** Reference exact make targets, file paths, commands.
|
||||||
|
- **Be concise.** Each section should be scannable in under 30 seconds.
|
||||||
|
- **Be actionable.** Every paragraph should tell the agent what to DO.
|
||||||
|
- **Use tables** for command reference, mappings, and comparisons.
|
||||||
|
- **Use code blocks** for commands the agent should run.
|
||||||
|
- **Link to other skills** when related (e.g., "See `dependency-graph` skill").
|
||||||
|
|
||||||
|
### Don't
|
||||||
|
- **No preamble.** Don't start with "This skill helps agents..." — just state what it does.
|
||||||
|
- **No filler.** Don't repeat information from AGENTS.md or other skills.
|
||||||
|
- **No vague advice.** "Be careful with branches" is useless. "Run `git branch --show-current` before every commit" is useful.
|
||||||
|
- **No AI slop.** Don't write "In this comprehensive guide, we will explore..." — just give the guidance.
|
||||||
|
- **No redundant sections.** If "Common Mistakes" would repeat "When to Invoke", skip it.
|
||||||
|
- **No marketing.** Don't describe the skill as "powerful" or "comprehensive".
|
||||||
|
|
||||||
|
## Scope Rules
|
||||||
|
|
||||||
|
- **One skill per concern.** Don't mix branch hygiene with CI monitoring.
|
||||||
|
- **Project-specific, not generic.** Skills reference this repo's make targets, file paths, and conventions — not abstract advice.
|
||||||
|
- **Shared skills must be identical across repos.** Use `SHARED_SKILLS` in the validator to enforce this.
|
||||||
|
- **Per-repo skills must reflect that repo's reality.** Don't copy infra-specific targets to sso-bridge.
|
||||||
|
|
||||||
|
## Automated Validation
|
||||||
|
|
||||||
|
Every skill must pass the validator (`tests/test_skills.py`). The validator checks:
|
||||||
|
|
||||||
|
1. **Structure** — H1 title, "When to Invoke" section, "Prerequisites" section
|
||||||
|
2. **Commands** — referenced `make <target>` commands exist in Makefile or devx.mak
|
||||||
|
3. **Paths** — referenced file paths exist in the repo
|
||||||
|
4. **Shared skills** — identical content across repos (SHA-256 comparison)
|
||||||
|
5. **No drift** — no references to nonexistent commands or files
|
||||||
|
|
||||||
|
Run the validator:
|
||||||
|
```bash
|
||||||
|
python3 tests/test_skills.py --repo infra --repo sso-bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
## Effectiveness Evaluation
|
||||||
|
|
||||||
|
### Static Checks (automated, CI)
|
||||||
|
|
||||||
|
The validator runs in CI as part of `make pytest-cov`. A failing skill
|
||||||
|
test blocks the PR. This catches:
|
||||||
|
- Missing sections
|
||||||
|
- Invalid commands
|
||||||
|
- Broken file references
|
||||||
|
- Cross-repo drift
|
||||||
|
|
||||||
|
### Runtime Metrics (manual, periodic)
|
||||||
|
|
||||||
|
Track these signals to evaluate skill effectiveness:
|
||||||
|
- **Skill invocation frequency** — how often agents invoke the skill
|
||||||
|
- **Success rate when invoked** — did the skill prevent the mistake it targets?
|
||||||
|
- **Feedback issues** — agents create Gitea issues with `feedback` label when a skill is unclear or wrong
|
||||||
|
- **Mistake recurrence** — if agents still make the mistake the skill targets, the skill needs improvement
|
||||||
|
|
||||||
|
### Retrospective Review
|
||||||
|
|
||||||
|
Periodically (monthly or after major incidents) review skills:
|
||||||
|
1. List all skills and their last-modified dates
|
||||||
|
2. Check for feedback issues tagged `skill-improvement`
|
||||||
|
3. Verify referenced commands still exist (run validator)
|
||||||
|
4. Remove skills that are no longer relevant
|
||||||
|
5. Update skills where mistakes still recur
|
||||||
|
6. Document lessons in this skill's "Common Mistakes" section
|
||||||
|
|
||||||
|
## Creating a New Skill — Checklist
|
||||||
|
|
||||||
|
- [ ] Identify the repeated struggle or non-obvious workflow
|
||||||
|
- [ ] Check no existing skill covers it
|
||||||
|
- [ ] Write the skill following the structure above
|
||||||
|
- [ ] Run `python3 tests/test_skills.py` — must pass
|
||||||
|
- [ ] Run `make pytest-cov` — must pass with 100% coverage
|
||||||
|
- [ ] If shared across repos, copy identical content to each repo
|
||||||
|
- [ ] Add the skill to `SHARED_SKILLS` in the validator if shared
|
||||||
|
- [ ] Create PR, verify CI passes, merge
|
||||||
@@ -2,6 +2,36 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.51.9] - 2026-08-26
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Exclude docs/plans/* from PR size check
|
||||||
|
|
||||||
|
## [0.51.8] - 2026-08-26
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Accept deps: as valid conventional commit type
|
||||||
|
|
||||||
|
## [0.51.7] - 2026-08-26
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Increase HTTP 500 retry count to 5 with longer backoff and visible logging
|
||||||
|
|
||||||
|
## [0.51.6] - 2026-08-26
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use stderr=STDOUT to capture all docker push output in one stream
|
||||||
|
|
||||||
|
## [0.51.5] - 2026-08-26
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Check stdout for HTTP 500 in _run_push (docker sends to stdout)
|
||||||
|
|
||||||
## [0.51.4] - 2026-08-26
|
## [0.51.4] - 2026-08-26
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ quality badges.
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
|
|
||||||
## Why devx?
|
## Why devx?
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.51.4",
|
"devx>=0.51.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
@@ -101,8 +101,8 @@ pip install -e .
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** If your project requires a specific devx version, pin it in
|
> **Note:** If your project requires a specific devx version, pin it in
|
||||||
> `dependencies` (for example, `"devx==0.51.4"`) or use a version constraint
|
> `dependencies` (for example, `"devx==0.51.9"`) or use a version constraint
|
||||||
> (for example, `"devx>=0.51.4,<0.52"`).
|
> (for example, `"devx>=0.51.9,<0.52"`).
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.51.4",
|
"devx>=0.51.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
||||||
```
|
```
|
||||||
|
|
||||||
Pin a specific version if needed: `"devx==0.51.4"` or `"devx>=0.51.4,<0.52"`.
|
Pin a specific version if needed: `"devx==0.51.9"` or `"devx>=0.51.9,<0.52"`.
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# DEVX-163: Fix _run_push to check stdout for HTTP 500
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
`_run_push` only checked `result.stderr` for HTTP 500, but docker push
|
||||||
|
sends the "received unexpected HTTP status: 500 Internal Server Error"
|
||||||
|
message to **stdout**, not stderr. This means the tenacity retry logic
|
||||||
|
added in DEVX-162 never triggered — the push failed immediately without
|
||||||
|
retrying.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
Check both `result.stdout` and `result.stderr` for the "500" status code.
|
||||||
|
Also update the "already exists" check in `push_image` to check both
|
||||||
|
streams, since docker may send that message to stdout as well.
|
||||||
|
|
||||||
|
REQ-1: _run_push checks both stdout and stderr for HTTP 500
|
||||||
|
REQ-2: push_image "already exists" check uses combined stdout+stderr
|
||||||
|
REQ-3: All existing tests pass with 100% coverage
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- Unit tests for stdout 500 detection
|
||||||
|
- Unit tests for stderr 500 detection
|
||||||
|
- Manual: trigger build-images workflow and verify retry works
|
||||||
|
|
||||||
|
## Deploy Plan
|
||||||
|
- Merge to master
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
- Revert the merge commit
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- [x] REQ-1: _run_push checks both stdout and stderr for HTTP 500
|
||||||
|
- [x] REQ-2: push_image "already exists" check uses combined stdout+stderr
|
||||||
|
- [x] REQ-3: All existing tests pass with 100% coverage
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# DEVX-164: Increase HTTP 500 retry count and backoff for docker push
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
The HTTP 500 retry logic (DEVX-162, DEVX-163) works correctly — 3 retry
|
||||||
|
attempts are made. But all 3 attempts fail because the Gitea registry's
|
||||||
|
"offset mismatch" race condition needs more than ~15s to recover. The
|
||||||
|
current backoff is 5s-20s with 3 attempts (total ~15s of waiting).
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
Increase retry count from 3 to 5 and backoff from 5-20s to 10-60s,
|
||||||
|
giving the registry up to ~2 minutes to recover. Add visible logging
|
||||||
|
between retry attempts so the CI logs show the retry happening.
|
||||||
|
|
||||||
|
REQ-1: Increase retry count from 3 to 5
|
||||||
|
REQ-2: Increase backoff from 5-20s to 10-60s exponential
|
||||||
|
REQ-3: Add visible logging between retry attempts (click.echo)
|
||||||
|
REQ-4: All tests pass with 100% coverage
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- Unit tests verify retry count and backoff parameters
|
||||||
|
- Unit tests verify logging output on retry
|
||||||
|
- Manual: trigger build-images workflow and verify retries visible in logs
|
||||||
|
|
||||||
|
## Deploy Plan
|
||||||
|
- Merge to master
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
- Revert the merge commit
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- [x] REQ-1: Increase retry count from 3 to 5
|
||||||
|
- [x] REQ-2: Increase backoff from 5-20s to 10-60s exponential
|
||||||
|
- [x] REQ-3: Add visible logging between retry attempts (click.echo)
|
||||||
|
- [x] REQ-4: All tests pass with 100% coverage
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# DEVX-165: Accept deps: as valid conventional commit type
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
The commit validator rejects `deps:` as a conventional commit type, causing
|
||||||
|
post-merge CI failures on grm and sso-bridge repos where automated dependency
|
||||||
|
bump PRs use `deps: bump devx...` as the commit message.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
REQ-1: Add `deps` to `CONVENTIONAL_RE` in `src/devx/config.py`
|
||||||
|
REQ-2: Update the allowed types list in the error message in
|
||||||
|
`src/devx/ci/validate_commit_msg.py`
|
||||||
|
REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
|
||||||
|
and `tests/unit/test_validate_commit_msg.py`
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- `make pytest-cov` passes with 100% coverage
|
||||||
|
- `make lint-all` passes
|
||||||
|
|
||||||
|
## Deploy Plan
|
||||||
|
- Merge to master → post-merge auto-publishes new devx version
|
||||||
|
- grm and sso-bridge bump devx version to pick up the fix
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
- Revert the merge commit
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- [x] REQ-1: Add `deps` to `CONVENTIONAL_RE` in `src/devx/config.py`
|
||||||
|
- [x] REQ-2: Update the allowed types list in the error message in
|
||||||
|
`src/devx/ci/validate_commit_msg.py`
|
||||||
|
- [x] REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
|
||||||
|
and `tests/unit/test_validate_commit_msg.py`
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# DEVX-166: Exclude docs/plans/* from PR size check
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Planning docs in `docs/plans/` are legitimately large (700+ lines) but
|
||||||
|
fail the PR size check (max 500 lines). This blocks PRs that only add
|
||||||
|
planning documents.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
REQ-1: Add `docs/plans/*` to `DEFAULT_EXCLUDED_PATTERNS` in
|
||||||
|
`src/devx/ci/check_pr_size.py`
|
||||||
|
REQ-2: Add test coverage for the new exclusion pattern
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- `make pytest-cov` passes with 100% coverage
|
||||||
|
- `make lint-all` passes
|
||||||
|
|
||||||
|
## Deploy Plan
|
||||||
|
- Merge to master → post-merge auto-publishes new devx version
|
||||||
|
- Infra PR #1179 picks up the fix once devx is bumped
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
- Revert the merge commit
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- [x] REQ-1: Add `docs/plans/*` to `DEFAULT_EXCLUDED_PATTERNS` in
|
||||||
|
`src/devx/ci/check_pr_size.py`
|
||||||
|
- [x] REQ-2: Add test coverage for the new exclusion pattern
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# DEVX-167: Add dependency-graph, deployment-coordination, and skill-creation skills
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Agents working across the oblachno ecosystem lack shared, persistent
|
||||||
|
context for three recurring pain points:
|
||||||
|
|
||||||
|
1. **Cross-repo dependency ordering** — agents frequently merge
|
||||||
|
downstream PRs before the upstream publish job completes, or forget
|
||||||
|
to bump infra. There is no single reference for which repo produces
|
||||||
|
what and in what order changes must propagate.
|
||||||
|
2. **devx release coordination** — devx is the base package pinned by
|
||||||
|
grm, sso-bridge, and infra. Agents repeatedly merge devx PRs and
|
||||||
|
immediately merge downstream bumps without waiting for the PyPI
|
||||||
|
publish job, or bump only one consumer when a change affects all
|
||||||
|
three.
|
||||||
|
3. **Skill quality drift** — skills are created ad hoc with inconsistent
|
||||||
|
structure, vague advice, and no automated validation reference. New
|
||||||
|
skills miss required sections, reference nonexistent make targets,
|
||||||
|
and drift across repos.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
Add three SKILL.md files under `.devin/skills/`:
|
||||||
|
|
||||||
|
REQ-1: `dependency-graph` — shared skill mapping the oblachno ecosystem
|
||||||
|
(repos, what each produces, consumers, release triggers, correct
|
||||||
|
cross-repo change order, state verification checklist)
|
||||||
|
|
||||||
|
REQ-2: `deployment-coordination` — devx-specific skill covering the
|
||||||
|
devx release flow, downstream consumers, manual bump procedure, and
|
||||||
|
common mistakes when coordinating a devx change
|
||||||
|
|
||||||
|
REQ-3: `skill-creation` — shared skill defining skill structure,
|
||||||
|
quality standards, scope rules, automated validation reference, and a
|
||||||
|
creation checklist
|
||||||
|
|
||||||
|
## Files Affected
|
||||||
|
- `.devin/skills/dependency-graph/SKILL.md` (new)
|
||||||
|
- `.devin/skills/deployment-coordination/SKILL.md` (new)
|
||||||
|
- `.devin/skills/skill-creation/SKILL.md` (new)
|
||||||
|
- `docs/specs/DEVX-167.md` (new)
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- Verify all three SKILL.md files follow the required structure (H1
|
||||||
|
title, When to Invoke, Prerequisites sections)
|
||||||
|
- Verify referenced make targets and file paths exist
|
||||||
|
- Run `make pytest-cov` — skill validation tests must pass
|
||||||
|
|
||||||
|
## Deploy Plan
|
||||||
|
- Merge to master; skills are consumed by agents immediately on next
|
||||||
|
invocation — no build or deploy step required
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
- Revert the merge commit; remove the three skill directories
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- [x] REQ-1: dependency-graph skill exists with ecosystem map, repo
|
||||||
|
table, dependency chain, cross-repo change order, and state
|
||||||
|
verification checklist
|
||||||
|
- [x] REQ-2: deployment-coordination skill exists with devx release
|
||||||
|
flow, downstream consumer table, coordination steps, and common
|
||||||
|
mistakes
|
||||||
|
- [x] REQ-3: skill-creation skill exists with structure template,
|
||||||
|
quality standards, scope rules, validation reference, and
|
||||||
|
creation checklist
|
||||||
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.51.4",
|
"devx>=0.51.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"devx>=0.51.4",
|
"devx>=0.51.9",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ create_dependency_pr, auto_merge, release, publish), developer tooling
|
|||||||
molecule testing helpers for Ansible projects.
|
molecule testing helpers for Ansible projects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.51.4"
|
__version__ = "0.51.9"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ DEFAULT_EXCLUDED_PATTERNS = [
|
|||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"README.md",
|
"README.md",
|
||||||
"docs/index.md",
|
"docs/index.md",
|
||||||
|
"docs/plans/*",
|
||||||
"*.svg",
|
"*.svg",
|
||||||
"uv.lock",
|
"uv.lock",
|
||||||
"poetry.lock",
|
"poetry.lock",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ def main(commit_msg_file: str | None, branch: str | None, from_git: bool) -> Non
|
|||||||
" Expected: <type>: <description>\n"
|
" Expected: <type>: <description>\n"
|
||||||
" Got: {subject}\n"
|
" Got: {subject}\n"
|
||||||
" Allowed types: feat, fix, chore, docs, style, refactor,\n"
|
" Allowed types: feat, fix, chore, docs, style, refactor,\n"
|
||||||
" perf, test, ci, build, revert, BREAKING CHANGE",
|
" perf, test, ci, build, deps, revert, BREAKING CHANGE",
|
||||||
subject=subject,
|
subject=subject,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -93,4 +93,4 @@ RETRY_BACKOFF_BASE = 2 # seconds: 2, 4, 8
|
|||||||
RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
|
RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
|
||||||
|
|
||||||
# Conventional commit regex — used by validate_commit_msg.py
|
# Conventional commit regex — used by validate_commit_msg.py
|
||||||
CONVENTIONAL_RE = re.compile(r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert)(\(.+\))?: .+")
|
CONVENTIONAL_RE = re.compile(r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert|deps)(\(.+\))?: .+")
|
||||||
|
|||||||
@@ -266,15 +266,22 @@ def _run_push(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|||||||
BlobUploader.Append that causes intermittent HTTP 500 "offset
|
BlobUploader.Append that causes intermittent HTTP 500 "offset
|
||||||
mismatch" errors during concurrent blob uploads. Retrying the
|
mismatch" errors during concurrent blob uploads. Retrying the
|
||||||
push gives the registry time to recover.
|
push gives the registry time to recover.
|
||||||
|
|
||||||
|
Docker sends push progress/errors to both stdout and stderr depending
|
||||||
|
on the error type, so both streams are checked for the 500 status.
|
||||||
|
Uses stderr=STDOUT to merge both streams into stdout, ensuring all
|
||||||
|
output is captured in one place (docker push output behavior varies
|
||||||
|
depending on TTY detection).
|
||||||
"""
|
"""
|
||||||
result = subprocess.run( # nosec B603
|
result = subprocess.run( # nosec B603
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
if result.returncode != 0 and "500" in result.stderr:
|
if result.returncode != 0 and "500" in (result.stdout or ""):
|
||||||
raise PushHTTP500Error(result.stderr.strip())
|
raise PushHTTP500Error(result.stdout.strip())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -308,9 +315,17 @@ def push_image(
|
|||||||
click.echo(f"Pushing {ft}...")
|
click.echo(f"Pushing {ft}...")
|
||||||
|
|
||||||
@retry(
|
@retry(
|
||||||
stop=stop_after_attempt(3),
|
stop=stop_after_attempt(5),
|
||||||
wait=wait_exponential(multiplier=5, min=5, max=20),
|
wait=wait_exponential(multiplier=10, min=10, max=60),
|
||||||
retry=retry_if_exception_type(PushHTTP500Error),
|
retry=retry_if_exception_type(PushHTTP500Error),
|
||||||
|
before_sleep=lambda retry_state: click.echo(
|
||||||
|
_(
|
||||||
|
" HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...",
|
||||||
|
wait=retry_state.next_action.sleep if retry_state.next_action else 0,
|
||||||
|
attempt=retry_state.attempt_number + 1,
|
||||||
|
),
|
||||||
|
err=True,
|
||||||
|
),
|
||||||
reraise=True,
|
reraise=True,
|
||||||
)
|
)
|
||||||
def _attempt(_cmd: list[str] = cmd) -> subprocess.CompletedProcess[str]:
|
def _attempt(_cmd: list[str] = cmd) -> subprocess.CompletedProcess[str]:
|
||||||
@@ -329,10 +344,10 @@ def push_image(
|
|||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
click.echo(f"Pushed {ft}")
|
click.echo(f"Pushed {ft}")
|
||||||
continue
|
continue
|
||||||
stderr = result.stderr.strip()
|
combined_output = (result.stdout or "").strip()
|
||||||
# Gitea #31964: push fails because tag already exists.
|
# Gitea #31964: push fails because tag already exists.
|
||||||
# Delete the old manifest and retry once.
|
# Delete the old manifest and retry once.
|
||||||
if username and token and "already exists" in stderr.lower():
|
if username and token and "already exists" in combined_output.lower():
|
||||||
click.echo(" Tag exists (Gitea #31964), deleting old manifest and retrying...")
|
click.echo(" Tag exists (Gitea #31964), deleting old manifest and retrying...")
|
||||||
delete_remote_manifest(
|
delete_remote_manifest(
|
||||||
registry,
|
registry,
|
||||||
@@ -345,16 +360,17 @@ def push_image(
|
|||||||
click.echo(f" Retrying push {ft}...")
|
click.echo(f" Retrying push {ft}...")
|
||||||
result = subprocess.run( # nosec B603
|
result = subprocess.run( # nosec B603
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
click.echo(f"Pushed {ft} (after retry)")
|
click.echo(f"Pushed {ft} (after retry)")
|
||||||
continue
|
continue
|
||||||
stderr = result.stderr.strip()
|
combined_output = (result.stdout or "").strip()
|
||||||
click.echo(
|
click.echo(
|
||||||
_("Push failed for {tag}: {error}", tag=ft, error=stderr),
|
_("Push failed for {tag}: {error}", tag=ft, error=combined_output),
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
all_ok = False
|
all_ok = False
|
||||||
|
|||||||
@@ -2999,13 +2999,13 @@
|
|||||||
"PR number for label check": "PR number for label check",
|
"PR number for label check": "PR number for label check",
|
||||||
"Repo (owner/name) for label check": "Repo (owner/name) for label check"
|
"Repo (owner/name) for label check": "Repo (owner/name) for label check"
|
||||||
},
|
},
|
||||||
"Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE": {
|
"Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE": {
|
||||||
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
|
||||||
"de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
|
||||||
"en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
|
||||||
"pl": "Ups! Wiadomość commit musi być w formacie conventional commit.\n Oczekiwano: <typ>: <opis>\n Otrzymano: {subject}\n Dozwolone typy: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"pl": "Ups! Wiadomość commit musi być w formacie conventional commit.\n Oczekiwano: <typ>: <opis>\n Otrzymano: {subject}\n Dozwolone typy: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
|
||||||
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
|
||||||
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
|
||||||
"PR number for label check": "PR number for label check",
|
"PR number for label check": "PR number for label check",
|
||||||
"Repo (owner/name) for label check": "Repo (owner/name) for label check"
|
"Repo (owner/name) for label check": "Repo (owner/name) for label check"
|
||||||
},
|
},
|
||||||
@@ -5152,5 +5152,13 @@
|
|||||||
"pl": "PR has 'refactoring' label — size check bypassed.",
|
"pl": "PR has 'refactoring' label — size check bypassed.",
|
||||||
"ru": "PR has 'refactoring' label — size check bypassed.",
|
"ru": "PR has 'refactoring' label — size check bypassed.",
|
||||||
"zh": "PR has 'refactoring' label — size check bypassed."
|
"zh": "PR has 'refactoring' label — size check bypassed."
|
||||||
|
},
|
||||||
|
" HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...": {
|
||||||
|
"en": "HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...",
|
||||||
|
"bg": " HTTP 500 от регистъра, повторен опит след {wait:.0f}с (опит {attempt}/5)...",
|
||||||
|
"de": " HTTP 500 vom Registry, Wiederholung in {wait:.0f}s (Versuch {attempt}/5)...",
|
||||||
|
"pl": " HTTP 500 z rejestru, ponawianie za {wait:.0f}s (próba {attempt}/5)...",
|
||||||
|
"ru": " HTTP 500 от реестра, повтор через {wait:.0f}с (попытка {attempt}/5)...",
|
||||||
|
"zh": " 注册表返回 HTTP 500,{wait:.0f}秒后重试(第{attempt}/5次尝试)..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ class TestBuildImage:
|
|||||||
class TestPushImage:
|
class TestPushImage:
|
||||||
def test_success(self) -> None:
|
def test_success(self) -> None:
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
||||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
mock_result = MagicMock(returncode=0, stdout="")
|
||||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
||||||
assert push_image(spec, "git.example.com") is True
|
assert push_image(spec, "git.example.com") is True
|
||||||
assert mock_run.call_count == 2
|
assert mock_run.call_count == 2
|
||||||
@@ -206,8 +206,8 @@ class TestPushImage:
|
|||||||
def test_partial_failure(self) -> None:
|
def test_partial_failure(self) -> None:
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
||||||
results = [
|
results = [
|
||||||
MagicMock(returncode=0, stderr="", stdout=""),
|
MagicMock(returncode=0, stdout=""),
|
||||||
MagicMock(returncode=1, stderr="push failed", stdout=""),
|
MagicMock(returncode=1, stdout="push failed"),
|
||||||
]
|
]
|
||||||
with patch("devx.tools.build_image.subprocess.run", side_effect=results):
|
with patch("devx.tools.build_image.subprocess.run", side_effect=results):
|
||||||
assert push_image(spec, "git.example.com") is False
|
assert push_image(spec, "git.example.com") is False
|
||||||
@@ -221,7 +221,7 @@ class TestPushImage:
|
|||||||
def test_no_delete_on_success_with_creds(self) -> None:
|
def test_no_delete_on_success_with_creds(self) -> None:
|
||||||
"""Push-first: no delete needed when push succeeds."""
|
"""Push-first: no delete needed when push succeeds."""
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
mock_result = MagicMock(returncode=0, stdout="")
|
||||||
with (
|
with (
|
||||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
||||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
||||||
@@ -231,7 +231,7 @@ class TestPushImage:
|
|||||||
|
|
||||||
def test_no_delete_without_creds(self) -> None:
|
def test_no_delete_without_creds(self) -> None:
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
mock_result = MagicMock(returncode=0, stdout="")
|
||||||
with (
|
with (
|
||||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
||||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
||||||
@@ -243,8 +243,8 @@ class TestPushImage:
|
|||||||
"""Gitea #31964: push fails with 'already exists', delete + retry."""
|
"""Gitea #31964: push fails with 'already exists', delete + retry."""
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||||
results = [
|
results = [
|
||||||
MagicMock(returncode=1, stderr="package version already exists", stdout=""),
|
MagicMock(returncode=1, stdout="package version already exists"),
|
||||||
MagicMock(returncode=0, stderr="", stdout=""),
|
MagicMock(returncode=0, stdout=""),
|
||||||
]
|
]
|
||||||
with (
|
with (
|
||||||
patch("devx.tools.build_image.subprocess.run", side_effect=results),
|
patch("devx.tools.build_image.subprocess.run", side_effect=results),
|
||||||
@@ -263,7 +263,7 @@ class TestPushImage:
|
|||||||
def test_no_delete_on_non_already_exists_failure(self) -> None:
|
def test_no_delete_on_non_already_exists_failure(self) -> None:
|
||||||
"""Push fails for other reasons (non-500) — old manifest preserved."""
|
"""Push fails for other reasons (non-500) — old manifest preserved."""
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||||
mock_result = MagicMock(returncode=1, stderr="denied: requested access to the resource is denied", stdout="")
|
mock_result = MagicMock(returncode=1, stdout="denied: requested access to the resource is denied")
|
||||||
with (
|
with (
|
||||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
||||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
||||||
@@ -275,8 +275,8 @@ class TestPushImage:
|
|||||||
"""Gitea #31964 retry also fails — both pushes fail."""
|
"""Gitea #31964 retry also fails — both pushes fail."""
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||||
results = [
|
results = [
|
||||||
MagicMock(returncode=1, stderr="package version already exists", stdout=""),
|
MagicMock(returncode=1, stdout="package version already exists"),
|
||||||
MagicMock(returncode=1, stderr="push failed again", stdout=""),
|
MagicMock(returncode=1, stdout="push failed again"),
|
||||||
]
|
]
|
||||||
with (
|
with (
|
||||||
patch("devx.tools.build_image.subprocess.run", side_effect=results),
|
patch("devx.tools.build_image.subprocess.run", side_effect=results),
|
||||||
@@ -288,8 +288,8 @@ class TestPushImage:
|
|||||||
"""HTTP 500 from registry race condition — retry succeeds."""
|
"""HTTP 500 from registry race condition — retry succeeds."""
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||||
results = [
|
results = [
|
||||||
MagicMock(returncode=1, stderr="received unexpected HTTP status: 500 Internal Server Error", stdout=""),
|
MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error"),
|
||||||
MagicMock(returncode=0, stderr="", stdout=""),
|
MagicMock(returncode=0, stdout=""),
|
||||||
]
|
]
|
||||||
with (
|
with (
|
||||||
patch("devx.tools.build_image.subprocess.run", side_effect=results),
|
patch("devx.tools.build_image.subprocess.run", side_effect=results),
|
||||||
@@ -302,9 +302,7 @@ class TestPushImage:
|
|||||||
def test_http_500_retries_all_fail(self) -> None:
|
def test_http_500_retries_all_fail(self) -> None:
|
||||||
"""HTTP 500 retries exhausted — push fails, no delete attempted."""
|
"""HTTP 500 retries exhausted — push fails, no delete attempted."""
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||||
mock_result = MagicMock(
|
mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
|
||||||
returncode=1, stderr="received unexpected HTTP status: 500 Internal Server Error", stdout=""
|
|
||||||
)
|
|
||||||
with (
|
with (
|
||||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
||||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
||||||
@@ -314,19 +312,19 @@ class TestPushImage:
|
|||||||
mock_del.assert_not_called()
|
mock_del.assert_not_called()
|
||||||
|
|
||||||
def test_run_push_raises_on_500(self) -> None:
|
def test_run_push_raises_on_500(self) -> None:
|
||||||
"""_run_push raises PushHTTP500Error when stderr contains 500."""
|
"""_run_push raises PushHTTP500Error when stdout contains 500."""
|
||||||
from devx.tools.build_image import _run_push
|
from devx.tools.build_image import _run_push
|
||||||
|
|
||||||
mock_result = MagicMock(returncode=1, stderr="HTTP 500 Internal Server Error", stdout="")
|
mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
|
||||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||||
with pytest.raises(PushHTTP500Error, match="HTTP 500"):
|
with pytest.raises(PushHTTP500Error, match="500"):
|
||||||
_run_push(["docker", "push", "img:latest"])
|
_run_push(["docker", "push", "img:latest"])
|
||||||
|
|
||||||
def test_run_push_no_raise_on_non_500(self) -> None:
|
def test_run_push_no_raise_on_non_500(self) -> None:
|
||||||
"""_run_push returns result when stderr has no 500."""
|
"""_run_push returns result when stdout has no 500."""
|
||||||
from devx.tools.build_image import _run_push
|
from devx.tools.build_image import _run_push
|
||||||
|
|
||||||
mock_result = MagicMock(returncode=1, stderr="denied: access denied", stdout="")
|
mock_result = MagicMock(returncode=1, stdout="denied: access denied")
|
||||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||||
result = _run_push(["docker", "push", "img:latest"])
|
result = _run_push(["docker", "push", "img:latest"])
|
||||||
assert result.returncode == 1
|
assert result.returncode == 1
|
||||||
@@ -335,7 +333,7 @@ class TestPushImage:
|
|||||||
"""_run_push returns result on success."""
|
"""_run_push returns result on success."""
|
||||||
from devx.tools.build_image import _run_push
|
from devx.tools.build_image import _run_push
|
||||||
|
|
||||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
mock_result = MagicMock(returncode=0, stdout="")
|
||||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||||
result = _run_push(["docker", "push", "img:latest"])
|
result = _run_push(["docker", "push", "img:latest"])
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ class TestIsExcluded:
|
|||||||
def test_excludes_readme(self) -> None:
|
def test_excludes_readme(self) -> None:
|
||||||
assert is_excluded("README.md", ["README.md"])
|
assert is_excluded("README.md", ["README.md"])
|
||||||
|
|
||||||
|
def test_excludes_plans_glob(self) -> None:
|
||||||
|
assert is_excluded("docs/plans/sso-bridge-full-extraction.md", ["docs/plans/*"])
|
||||||
|
|
||||||
|
def test_does_not_exclude_specs(self) -> None:
|
||||||
|
assert not is_excluded("docs/specs/DEVX-165.md", ["docs/plans/*"])
|
||||||
|
|
||||||
|
|
||||||
class TestCheckSize:
|
class TestCheckSize:
|
||||||
def test_under_limits_passes(self) -> None:
|
def test_under_limits_passes(self) -> None:
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class TestConfigConstants:
|
|||||||
def test_conventional_re(self) -> None:
|
def test_conventional_re(self) -> None:
|
||||||
assert CONVENTIONAL_RE.match("feat: add feature")
|
assert CONVENTIONAL_RE.match("feat: add feature")
|
||||||
assert CONVENTIONAL_RE.match("fix(scope): bug fix")
|
assert CONVENTIONAL_RE.match("fix(scope): bug fix")
|
||||||
|
assert CONVENTIONAL_RE.match("deps: bump devx from v0.50.2 to v0.51.0")
|
||||||
assert not CONVENTIONAL_RE.match("random message")
|
assert not CONVENTIONAL_RE.match("random message")
|
||||||
assert not CONVENTIONAL_RE.match("feat:")
|
assert not CONVENTIONAL_RE.match("feat:")
|
||||||
assert not CONVENTIONAL_RE.match("BREAKING CHANGE: something")
|
assert not CONVENTIONAL_RE.match("BREAKING CHANGE: something")
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TestHelpers:
|
|||||||
assert CONVENTIONAL_RE.match("test: add tests")
|
assert CONVENTIONAL_RE.match("test: add tests")
|
||||||
assert CONVENTIONAL_RE.match("ci: update workflow")
|
assert CONVENTIONAL_RE.match("ci: update workflow")
|
||||||
assert CONVENTIONAL_RE.match("build: update deps")
|
assert CONVENTIONAL_RE.match("build: update deps")
|
||||||
|
assert CONVENTIONAL_RE.match("deps: bump devx from v0.50.2 to v0.51.0")
|
||||||
assert CONVENTIONAL_RE.match("revert: undo change")
|
assert CONVENTIONAL_RE.match("revert: undo change")
|
||||||
|
|
||||||
def test_conventional_re_allows_scope(self) -> None:
|
def test_conventional_re_allows_scope(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user