mdfetch 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.
- mdfetch-0.5.2/.github/workflows/publish.yml +120 -0
- mdfetch-0.5.2/.specify/feature.json +3 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/changelog.md +20 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/plan.md +39 -1
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/spec.md +83 -1
- {mdfetch-0.5.0 → mdfetch-0.5.2}/CLAUDE.md +5 -4
- {mdfetch-0.5.0 → mdfetch-0.5.2}/PKG-INFO +11 -1
- {mdfetch-0.5.0 → mdfetch-0.5.2}/README.md +10 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/pyproject.toml +1 -1
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/checklists/requirements.md +36 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/contracts/formula.md +119 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/contracts/tap-update-job.md +86 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/data-model.md +66 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/plan.md +152 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/quickstart.md +76 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/research.md +116 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/spec.md +121 -0
- mdfetch-0.5.2/specs/009-homebrew-tap-formula/tasks.md +172 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/base.py +33 -10
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/cli.py +23 -3
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/devto.py +9 -9
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/medium.py +11 -3
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/substack.py +10 -10
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/router.py +7 -6
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_cli.py +25 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_medium_extractor.py +23 -6
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_router.py +20 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/uv.lock +1 -1
- mdfetch-0.5.0/.github/workflows/publish.yml +0 -55
- mdfetch-0.5.0/.specify/feature.json +0 -3
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-analyze/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-archive-run/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-checklist/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-clarify/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-constitution/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-implement/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-plan/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-reconcile-run/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-specify/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-tasks/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.analyze.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.archive.run.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.checklist.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.clarify.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.constitution.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.implement.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.plan.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.reconcile.run.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.specify.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.tasks.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.taskstoissues.toml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gitattributes +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.github/copilot-instructions.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.github/workflows/ci.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.github/workflows/integration.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.gitignore +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.python-version +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/.registry +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/LICENSE +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/README.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/commands/archive.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/extension.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/README.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/config-template.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/extension.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/git-config.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/LICENSE +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/README.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/commands/reconcile.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/extension.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/init-options.json +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integration.json +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integrations/claude.manifest.json +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integrations/gemini.manifest.json +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integrations/speckit.manifest.json +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/constitution.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/common.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/setup-plan.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/setup-tasks.sh +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/checklist-template.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/constitution-template.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/plan-template.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/spec-template.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/tasks-template.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/workflows/speckit/workflow.yml +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/workflows/workflow-registry.json +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/.vscode/settings.json +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/GEMINI.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/LICENSE +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/Makefile +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/contracts/api.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/data-model.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/quickstart.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/contracts/public-api.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/data-model.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/quickstart.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/contracts/extract-api.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/contracts/extractor-api.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/data-model.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/quickstart.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/contracts/public-api.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/data-model.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/quickstart.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/contracts/public-api.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/data-model.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/quickstart.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/checklists/requirements.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/contracts/cli.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/data-model.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/plan.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/quickstart.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/research.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/spec.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/__init__.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/exceptions.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/__init__.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/dzone.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/thenewstack.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/__init__.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/conftest.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/__init__.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/conftest.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/architecting-the-asynchronous-agent.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/devto-integration-digest-december-2025.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/devto-integration-digest-july-2025.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/devto-integration-digest-march-2026.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/dzone-image-classification-pipeline-camel-djl.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/dzone-integration-patterns-fail-production.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/dzone-kiro-feature-to-requirements-design-tasks.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/from-drift-to-parity.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/integration-digest-december-2025.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/substack-api-trends-2025.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/substack-kafka-topic-types.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-api-mcp-agent.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-async-apis.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-developer-portal-api.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-json-schema-ai.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-mcp-api-governance.md +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_cli_integration.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_devto_integration.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_dzone_integration.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_medium_integration.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_substack_integration.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_thenewstack_integration.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/__init__.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_devto_extractor.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_dzone_extractor.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_fetch_errors.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_silent.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_substack_extractor.py +0 -0
- {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_thenewstack_extractor.py +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build distribution
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
if: github.event.release.target_commitish == 'main'
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v4
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
run: uv python pin 3.12
|
|
21
|
+
- name: Validate version
|
|
22
|
+
run: |
|
|
23
|
+
TAG_VERSION=${GITHUB_REF_NAME#v}
|
|
24
|
+
TOML_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
25
|
+
if [ "$TAG_VERSION" != "$TOML_VERSION" ]; then
|
|
26
|
+
echo "Error: Tag version ($TAG_VERSION) does not match pyproject.toml version ($TOML_VERSION)"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
- name: Build package
|
|
30
|
+
run: make build
|
|
31
|
+
- name: Store the distribution packages
|
|
32
|
+
uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: python-package-distributions
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
name: Publish to PyPI
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
needs: build
|
|
41
|
+
environment:
|
|
42
|
+
name: pypi
|
|
43
|
+
url: https://pypi.org/p/mdfetch
|
|
44
|
+
permissions:
|
|
45
|
+
id-token: write
|
|
46
|
+
steps:
|
|
47
|
+
- name: Download all the dists
|
|
48
|
+
uses: actions/download-artifact@v4
|
|
49
|
+
with:
|
|
50
|
+
name: python-package-distributions
|
|
51
|
+
path: dist/
|
|
52
|
+
- name: Publish package distributions to PyPI
|
|
53
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
54
|
+
with:
|
|
55
|
+
packages-dir: dist/
|
|
56
|
+
|
|
57
|
+
update-homebrew-tap:
|
|
58
|
+
name: Update Homebrew tap
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
needs: publish
|
|
61
|
+
concurrency:
|
|
62
|
+
group: homebrew-tap-update
|
|
63
|
+
cancel-in-progress: false
|
|
64
|
+
permissions:
|
|
65
|
+
contents: read
|
|
66
|
+
|
|
67
|
+
steps:
|
|
68
|
+
- name: Fetch sdist info from PyPI (with retry)
|
|
69
|
+
env:
|
|
70
|
+
VERSION: ${{ github.ref_name }}
|
|
71
|
+
run: |
|
|
72
|
+
VERSION="${VERSION#v}"
|
|
73
|
+
SHA256=""
|
|
74
|
+
for i in 1 2 3; do
|
|
75
|
+
RESPONSE=$(curl -sf "https://pypi.org/pypi/mdfetch/${VERSION}/json" || true)
|
|
76
|
+
if [ -n "$RESPONSE" ]; then
|
|
77
|
+
SDIST_URL=$(echo "$RESPONSE" | python3 -c \
|
|
78
|
+
"import sys,json; d=json.load(sys.stdin); \
|
|
79
|
+
urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
|
|
80
|
+
print(urls[0]['url'] if urls else '')" 2>/dev/null || true)
|
|
81
|
+
SHA256=$(echo "$RESPONSE" | python3 -c \
|
|
82
|
+
"import sys,json; d=json.load(sys.stdin); \
|
|
83
|
+
urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
|
|
84
|
+
print(urls[0]['digests']['sha256'] if urls else '')" 2>/dev/null || true)
|
|
85
|
+
if [ -n "$SHA256" ] && [ -n "$SDIST_URL" ]; then break; fi
|
|
86
|
+
fi
|
|
87
|
+
echo "PyPI not ready, retrying in 30s (attempt $i/3)..."
|
|
88
|
+
sleep 30
|
|
89
|
+
done
|
|
90
|
+
if [ -z "$SHA256" ]; then
|
|
91
|
+
echo "ERROR: Could not fetch sdist info from PyPI after 3 retries" >&2; exit 1
|
|
92
|
+
fi
|
|
93
|
+
echo "SDIST_URL=${SDIST_URL}" >> "$GITHUB_ENV"
|
|
94
|
+
echo "SHA256=${SHA256}" >> "$GITHUB_ENV"
|
|
95
|
+
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
|
96
|
+
|
|
97
|
+
- name: Clone homebrew-tap and update formula
|
|
98
|
+
env:
|
|
99
|
+
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
|
100
|
+
run: |
|
|
101
|
+
git clone "https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/stn1slv/homebrew-tap.git"
|
|
102
|
+
cd homebrew-tap
|
|
103
|
+
git config user.name "github-actions[bot]"
|
|
104
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
105
|
+
|
|
106
|
+
sed -i "s| url \"https://files.pythonhosted.org/packages/.*/mdfetch-.*\.tar\.gz\"| url \"${SDIST_URL}\"|" \
|
|
107
|
+
Formula/md-fetch.rb
|
|
108
|
+
sed -i "0,/ sha256 \"[a-f0-9]*\"/{s| sha256 \"[a-f0-9]*\"| sha256 \"${SHA256}\"|}" Formula/md-fetch.rb
|
|
109
|
+
|
|
110
|
+
if ! grep -qF " url \"${SDIST_URL}\"" Formula/md-fetch.rb; then
|
|
111
|
+
echo "ERROR: URL substitution made no change — formula URL pattern may have drifted" >&2; exit 1
|
|
112
|
+
fi
|
|
113
|
+
if ! grep -qF " sha256 \"${SHA256}\"" Formula/md-fetch.rb; then
|
|
114
|
+
echo "ERROR: SHA256 substitution made no change — formula sha256 pattern may have drifted" >&2; exit 1
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
git add Formula/md-fetch.rb
|
|
118
|
+
git commit -m "chore: bump md-fetch to v${VERSION}"
|
|
119
|
+
git pull --rebase
|
|
120
|
+
git push
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
### mdfetch — Homebrew Tap Formula — 2026-05-16
|
|
2
|
+
|
|
3
|
+
**Branch**: `009-homebrew-tap-formula`
|
|
4
|
+
**Spec**: specs/009-homebrew-tap-formula
|
|
5
|
+
|
|
6
|
+
**What was added**:
|
|
7
|
+
- `Formula/md-fetch.rb` in `stn1slv/homebrew-tap` using `Language::Python::Virtualenv` pattern with 13 resource blocks; `depends_on "python@3.12"`; `uses_from_macos "libxml2"` and `uses_from_macos "libxslt"` (required by lxml); test block verifying `md-fetch --version`
|
|
8
|
+
- `update-homebrew-tap` job in `.github/workflows/publish.yml`: polls PyPI JSON API (3×30s retry), patches formula url+sha256 via sed, commits and pushes to tap; `concurrency: group=homebrew-tap-update, cancel-in-progress: false` serializes concurrent release runs
|
|
9
|
+
- `README.md` updated with `brew install stn1slv/tap/md-fetch` as secondary install option beneath `pip install mdfetch`
|
|
10
|
+
- `homebrew-tap/README.md` updated with md-fetch row in Available Formulas table
|
|
11
|
+
- TAP_GITHUB_TOKEN documented as required fine-grained PAT (`Contents: read+write` on `stn1slv/homebrew-tap` only)
|
|
12
|
+
|
|
13
|
+
**New Components**:
|
|
14
|
+
- `Formula/md-fetch.rb` (in `stn1slv/homebrew-tap`) — Homebrew formula
|
|
15
|
+
- `update-homebrew-tap` CI job (in `.github/workflows/publish.yml`)
|
|
16
|
+
|
|
17
|
+
**Tasks Completed**: 13/15 (T002 manual PAT setup, T015 post-merge verification pending)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
1
21
|
### mdfetch — Architecture Refactoring & CLI Improvements — 2026-05-16
|
|
2
22
|
|
|
3
23
|
**Branch**: `refactor/reduce-duplication-and-robustness`
|
|
@@ -165,8 +165,46 @@ tests/
|
|
|
165
165
|
specs/ # Speckit feature specifications
|
|
166
166
|
pyproject.toml # hatchling build backend, uv package manager
|
|
167
167
|
Makefile # setup / test / integration / lint / typecheck / format / build / clean / upgrade-deps
|
|
168
|
+
.github/workflows/
|
|
169
|
+
├── ci.yml # lint + unit tests on push/PR (Python 3.12–3.14)
|
|
170
|
+
├── integration.yml # scheduled integration tests
|
|
171
|
+
└── publish.yml # PyPI publish + Homebrew tap update on release tag
|
|
168
172
|
```
|
|
169
173
|
|
|
174
|
+
**External repository: `stn1slv/homebrew-tap`** [009-homebrew-tap-formula]
|
|
175
|
+
```
|
|
176
|
+
Formula/
|
|
177
|
+
└── md-fetch.rb # Language::Python::Virtualenv formula; 13 resource blocks;
|
|
178
|
+
# depends_on "python@3.12"; uses_from_macos "libxml2" + "libxslt"
|
|
179
|
+
README.md # Available Formulas table updated with md-fetch row
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Homebrew Distribution Architecture [009-homebrew-tap-formula]
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
GitHub Release tag vX.Y.Z
|
|
188
|
+
↓
|
|
189
|
+
build job → publish job (PyPI)
|
|
190
|
+
↓
|
|
191
|
+
update-homebrew-tap job (NEW, needs: publish)
|
|
192
|
+
[concurrency: group=homebrew-tap-update, cancel-in-progress=false]
|
|
193
|
+
1. Poll https://pypi.org/pypi/mdfetch/{VERSION}/json (retry 3× / 30s)
|
|
194
|
+
2. Extract SDIST_URL and SHA256
|
|
195
|
+
3. Clone stn1slv/homebrew-tap via TAP_GITHUB_TOKEN (fine-grained PAT)
|
|
196
|
+
4. sed patch Formula/md-fetch.rb (url + first sha256 only)
|
|
197
|
+
5. git commit + pull --rebase + push
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Formula pattern**: `Language::Python::Virtualenv` — creates isolated venv in Cellar, installs 13 resource blocks, symlinks `libexec/bin/md-fetch` → `bin/md-fetch`.
|
|
201
|
+
|
|
202
|
+
**System library dependencies** (required by `lxml` resource):
|
|
203
|
+
- `uses_from_macos "libxml2"`
|
|
204
|
+
- `uses_from_macos "libxslt"`
|
|
205
|
+
|
|
206
|
+
**TAP_GITHUB_TOKEN**: Fine-grained PAT with `Contents: read+write` on `stn1slv/homebrew-tap` only. Stored as repository secret in `stn1slv/md-fetch`. Must NOT be a classic `repo`-scoped PAT.
|
|
207
|
+
|
|
170
208
|
---
|
|
171
209
|
|
|
172
210
|
## Constraints
|
|
@@ -261,4 +299,4 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
261
299
|
|
|
262
300
|
---
|
|
263
301
|
|
|
264
|
-
*Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/plan.md], [specs/005-substack-provider/plan.md], [specs/006-thenewstack-provider/plan.md]*
|
|
302
|
+
*Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/plan.md], [specs/005-substack-provider/plan.md], [specs/006-thenewstack-provider/plan.md], [specs/009-homebrew-tap-formula/plan.md]*
|
|
@@ -175,6 +175,43 @@ A developer passes a thenewstack.io URL that does not point to an article (e.g.,
|
|
|
175
175
|
|
|
176
176
|
---
|
|
177
177
|
|
|
178
|
+
### US-016 — Install md-fetch via Homebrew (P1)
|
|
179
|
+
[Source: specs/009-homebrew-tap-formula]
|
|
180
|
+
|
|
181
|
+
A macOS or Linux developer who does not want to manage a Python environment installs the `md-fetch` CLI using a single Homebrew command. After installation, the `md-fetch` binary is immediately available on the PATH without any additional configuration.
|
|
182
|
+
|
|
183
|
+
**Acceptance Scenarios**:
|
|
184
|
+
1. Given a macOS or Linux machine with Homebrew installed, when the user runs `brew install stn1slv/tap/md-fetch`, then the installation completes without errors and `md-fetch` is available on the PATH.
|
|
185
|
+
2. Given a successful installation, when the user runs `md-fetch --version`, then the command exits with code 0 and outputs the installed package version.
|
|
186
|
+
3. Given a successful installation, when the user runs `md-fetch <url>` with a supported URL, then the command returns Markdown output, confirming all dependencies are correctly bundled.
|
|
187
|
+
4. Given an existing installation, when the user runs `brew upgrade stn1slv/tap/md-fetch`, then the installation upgrades to the latest published version.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### US-017 — Formula Stays in Sync with PyPI Releases (P2)
|
|
192
|
+
[Source: specs/009-homebrew-tap-formula]
|
|
193
|
+
|
|
194
|
+
When a new version of `md-fetch` is published to PyPI, the Homebrew formula in `stn1slv/homebrew-tap` is updated automatically without any manual intervention from the maintainer. Users running `brew upgrade` receive the new version promptly.
|
|
195
|
+
|
|
196
|
+
**Acceptance Scenarios**:
|
|
197
|
+
1. Given a new version is successfully published to PyPI, when the release pipeline completes, then the formula in `stn1slv/homebrew-tap` reflects the new version number and correct integrity checksum.
|
|
198
|
+
2. Given a new version is successfully published to PyPI, when the release pipeline completes, then the tap update is committed and pushed without any manual step from the maintainer.
|
|
199
|
+
3. Given the automated tap update fails (e.g., token invalid, network error), when the pipeline completes, then the failure is reported as a visible CI job failure.
|
|
200
|
+
4. Given a published package, when a user on the previous formula version runs `brew upgrade`, then they receive the newly published version.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### US-018 — Discover Homebrew Installation via Documentation (P3)
|
|
205
|
+
[Source: specs/009-homebrew-tap-formula]
|
|
206
|
+
|
|
207
|
+
A new user reading the project README discovers that Homebrew is an installation option on macOS and can install `md-fetch` without reading further instructions.
|
|
208
|
+
|
|
209
|
+
**Acceptance Scenarios**:
|
|
210
|
+
1. Given the updated README, when a user visits the install section, then `brew install stn1slv/tap/md-fetch` is present as an installation option below the existing `pip install mdfetch` instruction.
|
|
211
|
+
2. Given the README install section, when a user follows the brew instruction verbatim, then the installation succeeds.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
178
215
|
### US-006 — Integration Tests Pass Against Real dev.to Article URLs (P3)
|
|
179
216
|
[Source: specs/002-devto-provider]
|
|
180
217
|
|
|
@@ -247,6 +284,17 @@ A developer runs the integration test suite and all dev.to integration tests pas
|
|
|
247
284
|
- **FR-049**: The library MUST convert embedded third-party content (e.g., YouTube video iframes) in thenewstack.io articles to plain anchor links using the embed's source URL, discarding the embed wrapper — matching the pattern used by existing providers. [Source: specs/006-thenewstack-provider]
|
|
248
285
|
- **FR-050**: The library MUST treat sponsored and native-advertising thenewstack.io article pages identically to editorial articles — extracting content as-is with no special detection, marking, or rejection. [Source: specs/006-thenewstack-provider]
|
|
249
286
|
|
|
287
|
+
### Homebrew Distribution
|
|
288
|
+
- **FR-051**: A Homebrew formula for the `md-fetch` package MUST be available in the `stn1slv/homebrew-tap` repository so that users can install the tool with standard Homebrew commands. [Source: specs/009-homebrew-tap-formula]
|
|
289
|
+
- **FR-052**: The formula MUST install a working `md-fetch` binary into Homebrew's `bin/` directory, making it immediately available on the user's PATH after installation. [Source: specs/009-homebrew-tap-formula]
|
|
290
|
+
- **FR-053**: The formula MUST bundle all runtime dependencies so that no external Python environment or manual dependency installation is required. [Source: specs/009-homebrew-tap-formula]
|
|
291
|
+
- **FR-054**: The formula MUST include a `brew test` block that verifies the installed binary runs successfully (`md-fetch --version` exits with code 0). [Source: specs/009-homebrew-tap-formula]
|
|
292
|
+
- **FR-055**: When a new version is published to PyPI, the release pipeline MUST automatically update the formula with the new version and correct integrity checksum. The checksum fetch MUST retry up to 3 times with 30-second intervals before failing. [Source: specs/009-homebrew-tap-formula]
|
|
293
|
+
- **FR-056**: The automated formula update MUST commit and push to `stn1slv/homebrew-tap` using a dedicated fine-grained PAT (`Contents: read+write` on that repository only). [Source: specs/009-homebrew-tap-formula]
|
|
294
|
+
- **FR-057**: If the automated formula update step fails for any reason, the failure MUST surface as a failed CI job, preventing silent divergence between PyPI and Homebrew. [Source: specs/009-homebrew-tap-formula]
|
|
295
|
+
- **FR-058**: The project README MUST include the `brew install stn1slv/tap/md-fetch` command as a secondary install option beneath `pip install mdfetch`. [Source: specs/009-homebrew-tap-formula]
|
|
296
|
+
- **FR-059**: The `TAP_GITHUB_TOKEN` secret (a fine-grained PAT with `Contents: read+write` on `stn1slv/homebrew-tap`) must be documented as a required repository secret for the automation to function. [Source: specs/009-homebrew-tap-formula]
|
|
297
|
+
|
|
250
298
|
### dev.to Platform
|
|
251
299
|
- **FR-015**: The library MUST add `dev.to` to the provider router so that any URL with the `dev.to` domain is dispatched to the dev.to provider without any change to the caller's code. [Source: specs/002-devto-provider]
|
|
252
300
|
- **FR-016**: The library MUST include a dev.to provider that fetches the article page, isolates the main article body from `<div id="article-body">`, removes all non-content elements (navigation, social reaction widgets, comments, author sidebar, tag links), and returns the body as Markdown. [Source: specs/002-devto-provider]
|
|
@@ -304,6 +352,23 @@ A developer runs the integration test suite and all dev.to integration tests pas
|
|
|
304
352
|
|
|
305
353
|
**Validation**: Page must contain `div#tns-post-body-content`; absent → `UnsupportedContentTypeError`. Body must yield non-empty text after stripping → else `EmptyContentError`. No paywall — all thenewstack.io articles are publicly accessible.
|
|
306
354
|
|
|
355
|
+
### Homebrew Formula
|
|
356
|
+
[Source: specs/009-homebrew-tap-formula]
|
|
357
|
+
| Attribute | Type | Description |
|
|
358
|
+
|-----------|------|-------------|
|
|
359
|
+
| `url` | `str` | PyPI sdist tarball URL, changes on every release |
|
|
360
|
+
| `sha256` | `str` | Hex SHA-256 of the sdist tarball, changes on every release |
|
|
361
|
+
| `depends_on` | `str` | `"python@3.12"` (static) |
|
|
362
|
+
| `uses_from_macos` | `list[str]` | `["libxml2", "libxslt"]` — required by `lxml` resource |
|
|
363
|
+
| `resource` blocks | 13 entries | All runtime dependencies pinned to versions in `uv.lock` at formula creation time |
|
|
364
|
+
|
|
365
|
+
**Mutable fields per release**: `url` and `sha256` only. All `resource` blocks are static until a separate manual dependency-bump PR.
|
|
366
|
+
|
|
367
|
+
### TAP_GITHUB_TOKEN
|
|
368
|
+
[Source: specs/009-homebrew-tap-formula]
|
|
369
|
+
|
|
370
|
+
A GitHub fine-grained Personal Access Token with `Contents: read+write` on `stn1slv/homebrew-tap` only. Stored as a repository secret in `stn1slv/md-fetch` and consumed exclusively by the `update-homebrew-tap` CI job.
|
|
371
|
+
|
|
307
372
|
### ExtractionResult (Output)
|
|
308
373
|
| Attribute | Type | Description |
|
|
309
374
|
|-----------|------|-------------|
|
|
@@ -379,6 +444,12 @@ caller provides URL string
|
|
|
379
444
|
- **thenewstack.io sponsored content**: `div.tns-sponsor-note` (mid-article sponsor injection) and three disclosure div variants are inside `div#tns-post-body-content` and must be decomposed before conversion. Sponsored article pages are extracted identically to editorial articles (FR-050). [Source: specs/006-thenewstack-provider]
|
|
380
445
|
- **thenewstack.io deck element**: `div.post-excerpt` is a `<div>`, not a semantic subtitle element; the extractor creates a new `<p>` tag with the deck text rather than copying the div directly, to ensure proper paragraph rendering. [Source: specs/006-thenewstack-provider]
|
|
381
446
|
- **thenewstack.io HTML structure changes**: If the site redesign moves content outside `div#tns-post-body-content`, the extractor will require an update.
|
|
447
|
+
- **Homebrew tap temporarily unavailable**: If `stn1slv/homebrew-tap` is unreachable when the pipeline tries to push, `git push` fails and the CI job surfaces a failure — the maintainer must retry manually. [Source: specs/009-homebrew-tap-formula]
|
|
448
|
+
- **TAP_GITHUB_TOKEN expired or revoked**: `git clone` or `git push` exits non-zero → CI job fails → maintainer is alerted to rotate the token. [Source: specs/009-homebrew-tap-formula]
|
|
449
|
+
- **PyPI release yanked after formula update**: The formula is not automatically rolled back; the maintainer handles yanked-release scenarios manually. [Source: specs/009-homebrew-tap-formula]
|
|
450
|
+
- **Concurrent releases**: Addressed by `concurrency: group=homebrew-tap-update, cancel-in-progress=false` — runs are serialized so the second release waits for the first tap-update to complete. [Source: specs/009-homebrew-tap-formula]
|
|
451
|
+
- **brew test fails after install**: `brew test md-fetch` exits non-zero when a transitive dependency is missing or `md-fetch --version` fails — installation validation fails. [Source: specs/009-homebrew-tap-formula]
|
|
452
|
+
- **Formula structure changed (sed no-op)**: If the formula is manually edited and the url/sha256 line format changes, `sed` produces no diff and `git commit` finds nothing staged → exits non-zero → CI job fails. [Source: specs/009-homebrew-tap-formula]
|
|
382
453
|
|
|
383
454
|
---
|
|
384
455
|
|
|
@@ -409,6 +480,12 @@ caller provides URL string
|
|
|
409
480
|
- **SC-029**: The extracted Markdown for any thenewstack.io article contains no consecutive blank-line runs of three or more lines. [Source: specs/006-thenewstack-provider]
|
|
410
481
|
- **SC-030**: The TheNewStack provider is exercised by integration tests using real network requests against all five reference article URLs, matching the pattern established by existing providers. [Source: specs/006-thenewstack-provider]
|
|
411
482
|
|
|
483
|
+
- **SC-031**: A user with Homebrew installed can complete the full `md-fetch` installation using a single command, with no Python environment setup required, in under 3 minutes on a standard broadband connection. [Source: specs/009-homebrew-tap-formula]
|
|
484
|
+
- **SC-032**: The Homebrew formula's built-in test (`brew test md-fetch`) passes on macOS and Linux after every installation. [Source: specs/009-homebrew-tap-formula]
|
|
485
|
+
- **SC-033**: The formula in `stn1slv/homebrew-tap` is updated within 10 minutes of PyPI publish confirmation — no manual maintainer action is required. [Source: specs/009-homebrew-tap-formula]
|
|
486
|
+
- **SC-034**: Formula update failures produce a visible CI job failure on every failed attempt, with zero silent failures. [Source: specs/009-homebrew-tap-formula]
|
|
487
|
+
- **SC-035**: The README install section includes the Homebrew installation command, enabling users to discover and use it without prior knowledge of the project's Python packaging. [Source: specs/009-homebrew-tap-formula]
|
|
488
|
+
|
|
412
489
|
---
|
|
413
490
|
|
|
414
491
|
## Assumptions
|
|
@@ -428,7 +505,12 @@ caller provides URL string
|
|
|
428
505
|
- **SC-015**: Zero changes are required in existing caller code to benefit from the Freedium fallback — existing integrations continue to work as-is. [Source: specs/003-medium-freedium-fallback]
|
|
429
506
|
- **SC-016**: When the primary Medium request succeeds, there is no additional latency attributable to the fallback mechanism. [Source: specs/003-medium-freedium-fallback]
|
|
430
507
|
- **SC-017**: All existing unit and integration tests for the Medium extractor continue to pass without modification after the fallback is introduced. [Source: specs/003-medium-freedium-fallback]
|
|
508
|
+
- Homebrew is already installed on the user's machine; the feature does not cover installing Homebrew itself. [Source: specs/009-homebrew-tap-formula]
|
|
509
|
+
- The formula targets the standard Homebrew installation on both macOS and Linux; Windows (WSL) is out of scope. [Source: specs/009-homebrew-tap-formula]
|
|
510
|
+
- Only the source distribution (sdist) is used as the formula's source artifact; binary wheels are not referenced by the formula. [Source: specs/009-homebrew-tap-formula]
|
|
511
|
+
- A `TAP_GITHUB_TOKEN` PAT will be created and added to repository secrets by the maintainer before first use — this is a one-time manual prerequisite. [Source: specs/009-homebrew-tap-formula]
|
|
512
|
+
- If a PyPI release is yanked after the formula has been updated, the formula is not automatically rolled back. [Source: specs/009-homebrew-tap-formula]
|
|
431
513
|
|
|
432
514
|
---
|
|
433
515
|
|
|
434
|
-
*Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/spec.md], [specs/005-substack-provider/spec.md], [specs/006-thenewstack-provider/spec.md]*
|
|
516
|
+
*Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/spec.md], [specs/005-substack-provider/spec.md], [specs/006-thenewstack-provider/spec.md], [specs/009-homebrew-tap-formula/spec.md]*
|
|
@@ -55,10 +55,6 @@ make typecheck # type check
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
<!-- SPECKIT START -->
|
|
58
|
-
## Active Feature Plan
|
|
59
|
-
|
|
60
|
-
Active feature: `008-mdfetch-cli`. Implementation plan: [`specs/008-mdfetch-cli/plan.md`](specs/008-mdfetch-cli/plan.md).
|
|
61
|
-
|
|
62
58
|
## Known Issues & Gotchas
|
|
63
59
|
|
|
64
60
|
### ⚠️ test_router.py "unsupported domain" fixture must be updated per new provider
|
|
@@ -75,4 +71,9 @@ Active feature: `008-mdfetch-cli`. Implementation plan: [`specs/008-mdfetch-cli/
|
|
|
75
71
|
**Issue:** VoxPop poll widgets (`div.tns-voxpop-screen`) appear to be inline article content but are actually page-level overlay modals.
|
|
76
72
|
**Root Cause:** VoxPop is injected outside `div#tns-post-body-content` — confirmed via live DOM inspection across all 5 reference articles.
|
|
77
73
|
**Prevention Rule:** Do not add VoxPop stripping to `TheNewStackExtractor.clean_html()`. If a future redesign moves VoxPop inside the body container, update `clean_html()` to decompose `div.tns-voxpop-screen`.
|
|
74
|
+
|
|
75
|
+
### ⚠️ Homebrew formula requires `uses_from_macos` for lxml
|
|
76
|
+
**Issue:** `brew audit --strict --new Formula/md-fetch.rb` fails with missing system library declarations when `lxml` is a resource block.
|
|
77
|
+
**Root Cause:** `lxml` requires `libxml2` and `libxslt`, which are macOS system libraries. Homebrew requires these to be declared explicitly via `uses_from_macos`.
|
|
78
|
+
**Prevention Rule:** Any Homebrew formula that includes `lxml` as a resource MUST declare `uses_from_macos "libxml2"` and `uses_from_macos "libxslt"` after the `depends_on` lines. Discovered via `brew audit --strict --new` during implementation.
|
|
78
79
|
<!-- SPECKIT END -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdfetch
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Extract article content from web platforms and return it as clean Markdown.
|
|
5
5
|
Project-URL: Homepage, https://github.com/stn1slv/md-fetch
|
|
6
6
|
Project-URL: Source, https://github.com/stn1slv/md-fetch
|
|
@@ -37,10 +37,20 @@ A Python library that extracts article content from web platforms and returns it
|
|
|
37
37
|
|
|
38
38
|
## Install
|
|
39
39
|
|
|
40
|
+
### pip
|
|
41
|
+
|
|
40
42
|
```bash
|
|
41
43
|
pip install mdfetch
|
|
42
44
|
```
|
|
43
45
|
|
|
46
|
+
### Homebrew (macOS / Linux)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
brew install stn1slv/tap/md-fetch
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
No Python environment setup required.
|
|
53
|
+
|
|
44
54
|
## CLI Usage
|
|
45
55
|
|
|
46
56
|
You can use the built-in `md-fetch` command directly from your terminal:
|
|
@@ -4,10 +4,20 @@ A Python library that extracts article content from web platforms and returns it
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
### pip
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
pip install mdfetch
|
|
9
11
|
```
|
|
10
12
|
|
|
13
|
+
### Homebrew (macOS / Linux)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
brew install stn1slv/tap/md-fetch
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
No Python environment setup required.
|
|
20
|
+
|
|
11
21
|
## CLI Usage
|
|
12
22
|
|
|
13
23
|
You can use the built-in `md-fetch` command directly from your terminal:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Specification Quality Checklist: Homebrew Tap Formula
|
|
2
|
+
|
|
3
|
+
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
|
4
|
+
**Created**: 2026-05-16
|
|
5
|
+
**Feature**: [spec.md](../spec.md)
|
|
6
|
+
|
|
7
|
+
## Content Quality
|
|
8
|
+
|
|
9
|
+
- [x] No implementation details (languages, frameworks, APIs)
|
|
10
|
+
- [x] Focused on user value and business needs
|
|
11
|
+
- [x] Written for non-technical stakeholders
|
|
12
|
+
- [x] All mandatory sections completed
|
|
13
|
+
|
|
14
|
+
## Requirement Completeness
|
|
15
|
+
|
|
16
|
+
- [x] No [NEEDS CLARIFICATION] markers remain
|
|
17
|
+
- [x] Requirements are testable and unambiguous
|
|
18
|
+
- [x] Success criteria are measurable
|
|
19
|
+
- [x] Success criteria are technology-agnostic (no implementation details)
|
|
20
|
+
- [x] All acceptance scenarios are defined
|
|
21
|
+
- [x] Edge cases are identified
|
|
22
|
+
- [x] Scope is clearly bounded
|
|
23
|
+
- [x] Dependencies and assumptions identified
|
|
24
|
+
|
|
25
|
+
## Feature Readiness
|
|
26
|
+
|
|
27
|
+
- [x] All functional requirements have clear acceptance criteria
|
|
28
|
+
- [x] User scenarios cover primary flows
|
|
29
|
+
- [x] Feature meets measurable outcomes defined in Success Criteria
|
|
30
|
+
- [x] No implementation details leak into specification
|
|
31
|
+
|
|
32
|
+
## Notes
|
|
33
|
+
|
|
34
|
+
- All items pass. Spec is ready for `/speckit-plan`.
|
|
35
|
+
- The formula's `Language::Python::Virtualenv` pattern and SHA256 mechanics are deliberately kept out of the spec (they are implementation decisions captured in the plan).
|
|
36
|
+
- The `TAP_GITHUB_TOKEN` prerequisite is documented as an assumption since it requires a one-time manual step outside the automated workflow.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Contract: Homebrew Formula (`Formula/md-fetch.rb`)
|
|
2
|
+
|
|
3
|
+
**Repository**: `stn1slv/homebrew-tap`
|
|
4
|
+
**Date**: 2026-05-16
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Defines the complete structure of the `md-fetch` Homebrew formula so that the implementation can be written without ambiguity.
|
|
9
|
+
|
|
10
|
+
## Formula Skeleton
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# typed: false
|
|
14
|
+
# frozen_string_literal: true
|
|
15
|
+
|
|
16
|
+
class MdFetch < Formula
|
|
17
|
+
include Language::Python::Virtualenv
|
|
18
|
+
|
|
19
|
+
desc "Extract article content from web platforms and return it as clean Markdown"
|
|
20
|
+
homepage "https://github.com/stn1slv/md-fetch"
|
|
21
|
+
url "https://files.pythonhosted.org/packages/<hash-path>/mdfetch-0.5.0.tar.gz"
|
|
22
|
+
sha256 "<mdfetch-0.5.0-sdist-sha256>"
|
|
23
|
+
license "MIT"
|
|
24
|
+
|
|
25
|
+
depends_on "python@3.12"
|
|
26
|
+
|
|
27
|
+
uses_from_macos "libxml2"
|
|
28
|
+
uses_from_macos "libxslt"
|
|
29
|
+
|
|
30
|
+
resource "anyio" do
|
|
31
|
+
url "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz"
|
|
32
|
+
sha256 "334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
resource "beautifulsoup4" do
|
|
36
|
+
url "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz"
|
|
37
|
+
sha256 "6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
resource "certifi" do
|
|
41
|
+
url "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz"
|
|
42
|
+
sha256 "8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
resource "click" do
|
|
46
|
+
url "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz"
|
|
47
|
+
sha256 "398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
resource "h11" do
|
|
51
|
+
url "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz"
|
|
52
|
+
sha256 "4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
resource "httpcore" do
|
|
56
|
+
url "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz"
|
|
57
|
+
sha256 "6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
resource "httpx" do
|
|
61
|
+
url "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz"
|
|
62
|
+
sha256 "75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
resource "idna" do
|
|
66
|
+
url "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz"
|
|
67
|
+
sha256 "ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
resource "lxml" do
|
|
71
|
+
url "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz"
|
|
72
|
+
sha256 "bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
resource "markdownify" do
|
|
76
|
+
url "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz"
|
|
77
|
+
sha256 "b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
resource "six" do
|
|
81
|
+
url "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz"
|
|
82
|
+
sha256 "ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
resource "soupsieve" do
|
|
86
|
+
url "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz"
|
|
87
|
+
sha256 "3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
resource "typing-extensions" do
|
|
91
|
+
url "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz"
|
|
92
|
+
sha256 "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def install
|
|
96
|
+
virtualenv_install_with_resources
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
test do
|
|
100
|
+
assert_match version.to_s, shell_output("#{bin}/md-fetch --version")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Invariants
|
|
106
|
+
|
|
107
|
+
- `url` and `sha256` (package) are the **only** fields updated on each mdfetch release.
|
|
108
|
+
- All `resource` blocks are updated only when runtime dependencies change (separate manual PR to homebrew-tap).
|
|
109
|
+
- The `test` block uses `assert_match version.to_s` so it automatically validates the installed binary reports the expected version.
|
|
110
|
+
|
|
111
|
+
## Pre-Implementation Note
|
|
112
|
+
|
|
113
|
+
The `url` path component for `mdfetch-0.5.0.tar.gz` (the `<hash-path>` placeholder above) must be retrieved from PyPI before writing the initial formula. Run:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
curl -s https://pypi.org/pypi/mdfetch/0.5.0/json | \
|
|
117
|
+
python3 -c "import sys,json; d=json.load(sys.stdin); \
|
|
118
|
+
[print(u['url'], u['digests']['sha256']) for u in d['urls'] if u['packagetype']=='sdist']"
|
|
119
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Contract: Tap-Update CI Job (`update-homebrew-tap`)
|
|
2
|
+
|
|
3
|
+
**Workflow file**: `.github/workflows/publish.yml` in `stn1slv/md-fetch`
|
|
4
|
+
**Date**: 2026-05-16
|
|
5
|
+
|
|
6
|
+
## Job Summary
|
|
7
|
+
|
|
8
|
+
A new `update-homebrew-tap` job is added to `publish.yml`, running after the existing `publish` job succeeds. It fetches the new release's sdist SHA-256 from PyPI, clones the homebrew-tap repo, patches the formula, and pushes the commit.
|
|
9
|
+
|
|
10
|
+
## Job Definition Skeleton
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
update-homebrew-tap:
|
|
14
|
+
name: Update Homebrew tap
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
needs: publish
|
|
17
|
+
concurrency:
|
|
18
|
+
group: homebrew-tap-update
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Fetch sdist info from PyPI (with retry)
|
|
25
|
+
env:
|
|
26
|
+
VERSION: ${{ github.ref_name }} # e.g. v0.5.1
|
|
27
|
+
run: |
|
|
28
|
+
VERSION="${VERSION#v}" # strip leading 'v'
|
|
29
|
+
SHA256=""
|
|
30
|
+
for i in 1 2 3; do
|
|
31
|
+
RESPONSE=$(curl -sf "https://pypi.org/pypi/mdfetch/${VERSION}/json" || true)
|
|
32
|
+
if [ -n "$RESPONSE" ]; then
|
|
33
|
+
SDIST_URL=$(echo "$RESPONSE" | python3 -c \
|
|
34
|
+
"import sys,json; d=json.load(sys.stdin); \
|
|
35
|
+
urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
|
|
36
|
+
print(urls[0]['url'] if urls else '')" 2>/dev/null || true)
|
|
37
|
+
SHA256=$(echo "$RESPONSE" | python3 -c \
|
|
38
|
+
"import sys,json; d=json.load(sys.stdin); \
|
|
39
|
+
urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
|
|
40
|
+
print(urls[0]['digests']['sha256'] if urls else '')" 2>/dev/null || true)
|
|
41
|
+
if [ -n "$SHA256" ] && [ -n "$SDIST_URL" ]; then break; fi
|
|
42
|
+
fi
|
|
43
|
+
echo "PyPI not ready, retrying in 30s (attempt $i/3)..."
|
|
44
|
+
sleep 30
|
|
45
|
+
done
|
|
46
|
+
if [ -z "$SHA256" ]; then
|
|
47
|
+
echo "ERROR: Could not fetch sdist info from PyPI after 3 retries" >&2; exit 1
|
|
48
|
+
fi
|
|
49
|
+
echo "SDIST_URL=${SDIST_URL}" >> "$GITHUB_ENV"
|
|
50
|
+
echo "SHA256=${SHA256}" >> "$GITHUB_ENV"
|
|
51
|
+
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
|
52
|
+
|
|
53
|
+
- name: Clone homebrew-tap and update formula
|
|
54
|
+
env:
|
|
55
|
+
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
|
56
|
+
run: |
|
|
57
|
+
git clone "https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/stn1slv/homebrew-tap.git"
|
|
58
|
+
cd homebrew-tap
|
|
59
|
+
git config user.name "github-actions[bot]"
|
|
60
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
61
|
+
|
|
62
|
+
# Replace the main package url and sha256 lines
|
|
63
|
+
sed -i "s| url \"https://files.pythonhosted.org/packages/.*/mdfetch-.*\.tar\.gz\"| url \"${SDIST_URL}\"|" \
|
|
64
|
+
Formula/md-fetch.rb
|
|
65
|
+
sed -i "0,/ sha256 \"[a-f0-9]*\"/{s| sha256 \"[a-f0-9]*\"| sha256 \"${SHA256}\"|}" Formula/md-fetch.rb
|
|
66
|
+
|
|
67
|
+
git add Formula/md-fetch.rb
|
|
68
|
+
git commit -m "chore: bump md-fetch to v${VERSION}"
|
|
69
|
+
git pull --rebase
|
|
70
|
+
git push
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Failure Modes and Expected Behavior
|
|
74
|
+
|
|
75
|
+
| Failure | Job Behaviour |
|
|
76
|
+
|---------|--------------|
|
|
77
|
+
| PyPI returns 404 for new version after 3×30s retries | Step exits non-zero → job fails → workflow shows red |
|
|
78
|
+
| `TAP_GITHUB_TOKEN` absent or invalid | `git clone` or `git push` exits non-zero → job fails |
|
|
79
|
+
| `sed` finds no matching line (formula structure changed) | `git diff --quiet` would show no changes; `git commit` would fail — job exits non-zero |
|
|
80
|
+
| Concurrent release overlap | `concurrency: group=homebrew-tap-update` serializes runs; second run queues and proceeds after first completes (`cancel-in-progress: false`) |
|
|
81
|
+
|
|
82
|
+
## Prerequisites
|
|
83
|
+
|
|
84
|
+
- `TAP_GITHUB_TOKEN` secret must be set in `stn1slv/md-fetch` repository settings.
|
|
85
|
+
- Token must be a **fine-grained PAT** with `Contents: read+write` on `stn1slv/homebrew-tap` only. Do not use a classic `repo`-scoped PAT.
|
|
86
|
+
- `jq` is **not** required — Python 3 (always available on `ubuntu-latest`) is used for JSON parsing.
|