This local maintainer roadmap starts from 0.1.27 and defines the next ten releases. Releases should normally be spaced two to three weeks apart. The sequence preserves every existing public contract while improving adoption, documentation, reliability, safe Markdown synchronization, and release security.
Targets by 1.0:
- At least 300 GitHub stars.
- At least three times the rolling 30-day downloads measured at the start of 0.2.0.
- No removal or incompatible change to existing Python APIs, CLI commands, JSON keys, environment variables, console scripts, or MCP tools.
- No package telemetry.
Starting state recorded on 2026-07-29:
- Version 0.1.27.
- 158 GitHub stars and 28 forks.
- Approximately 33.5k cumulative downloads reported by Pepy.
- 145 passing offline tests.
- Five console entry points.
- Python 3.10 through 3.14 support.
The rolling 30-day download baseline must be recorded immediately before 0.2.0 is released. Cumulative Pepy downloads are context only and are not the growth denominator.
The instruction "go ahead with the next release" authorizes implementation, validation, versioning, commit, tag creation, GitHub release creation, PyPI publication, post-publication verification, and the launch work specified for that release. This authority remains subject to credentials, external service availability, repository protections, and every release gate below.
If a gate fails, do not tag, publish, or announce the release. Report the
failure and leave the roadmap entry in progress.
Each release has one of these statuses:
planned: no release work has started.in progress: implementation or validation has started.released: the tagged package is published and post-publication checks pass.
When starting a release:
- Read this roadmap,
docs/releasing.md,CHANGELOG.md, and the preceding release note. - Select the first entry that is not
releasedand implement only that release. - Change its status to
in progressbefore making release-specific changes. - Preserve unrelated working-tree changes.
- Add regression tests before changing existing behavior.
- Keep package versions unchanged until implementation and validation are complete.
- Run the offline suite, strict marker collection, build validation, base-wheel smoke test, MCP-extra smoke test, and applicable live tests.
- Update
pyproject.toml,substack.__version__,CHANGELOG.md, the per-release page, and this roadmap together. - Commit and push the exact validated source.
- Create the version tag and GitHub release, publish to PyPI, install the published artifact in clean base and MCP environments, and execute the release's launch work.
- Mark the entry
releasedonly after the published artifact passes its checks. - Record release-time and approximately 14-day metrics before starting the following release.
- Preserve
Api,Post, current method names, argument meanings, and defaults. - Preserve all five console scripts.
- Preserve existing CLI commands, flags, confirmation behavior, exit codes, and JSON keys. Additive fields are allowed.
- Preserve
EMAIL,PASSWORD,PUBLICATION_URL,COOKIES_PATH, andCOOKIES_STRING. - Preserve existing MCP tool names and parameters, including current
publish_draftsemantics. - Existing behavior may be deprecated in documentation but cannot be removed before 2.0.
- Every fixed defect must receive a regression test.
- Release tags, package metadata, and
substack.__version__must agree. - Undocumented upstream Substack behavior remains best-effort. Compatible fixes for upstream changes are patch releases, not breaking changes.
Use these sources:
- GitHub stars and forks at release time.
- Rolling 30-day downloads from the PyPI Stats recent endpoint, which excludes known mirrors.
- GitHub views, unique visitors, clones, and referrers from the repository's 14-day traffic data.
- Measurements at release time and approximately 14 days later.
Do not add telemetry to the package, CLI, SDK, or MCP server.
| Release | Status | Released | Tag | Stars | Forks | 30-day downloads | 14-day unique visitors | 14-day unique clones | Announcement links |
|---|---|---|---|---|---|---|---|---|---|
| 0.2.0 | released | 2026-07-29 | v0.2.0 | 158 | 28 | 12,272 | 49 | 140 | GitHub; Substack |
| 0.3.0 | released | 2026-08-09 | v0.3.0 | 160 | 28 | 13,528 | 40 | 91 | GitHub |
| 0.4.0 | released | 2026-08-24 | v0.4.0 | - | - | - | - | - | GitHub |
| 0.5.0 | released | 2026-08-30 | v0.5.0 | 166 | 28 | 14,448 | 47 | 88 | GitHub |
| 0.6.0 | released | 2026-08-30 | v0.6.0 | 166 | 28 | 14,448 | 47 | 88 | GitHub; Substack |
| 0.7.0 | in progress | - | - | - | - | - | - | - | - |
| 0.8.0 | planned | - | - | - | - | - | - | - | - |
| 0.9.0 | planned | - | - | - | - | - | - | - | - |
| 0.10.0 | planned | - | - | - | - | - | - | - | - |
| 1.0.0 | planned | - | - | - | 300 target | - | 3x baseline target | - | - |
Status: released
Objective: Make the existing package easier to understand and safer to adopt before adding more functionality.
- Rewrite the README around the primary outcome: safely create, manage, schedule, and publish Substack drafts from Markdown through Python, CLI, or MCP.
- Put installation, a three-command Markdown-to-draft flow, draft-only safety, and a visual result above the fold.
- Move legacy CLI, low-level node construction, YAML details, and contributor instructions into focused documentation pages without deleting their content.
- Add
CONTRIBUTING.md,SECURITY.md, anddocs/compatibility.md. - Link this roadmap from the README.
- Upgrade the existing pre-commit hooks to currently supported compatible revisions after verifying them.
- Change the PyPI development classifier from Alpha to Beta.
- Establish
docs/metrics.mdwith the dated 0.2.0 baseline. - Migrate publishing from
PYPI_TOKENto PyPI Trusted Publishing:- Configure the PyPI publisher for owner
ma2za, repositorypython-substack, workflowci_publish.yml, and environmentpypi. - Give only the publish job
id-token: write. - Remove the long-lived token input only after OIDC publishing is configured.
- Protect the
pypiGitHub environment. - Follow the PyPI Trusted Publisher configuration.
- Configure the PyPI publisher for owner
- No runtime API changes.
- All existing README use cases remain reachable through documentation links.
- The existing 145 offline tests remain green.
- Test every README command that can run without credentials.
- Validate base and MCP wheel installations.
- Confirm the published artifact reports Trusted Publishing on PyPI.
- Publish a GitHub release and a Substack announcement repositioning the project as writer-side Markdown automation.
- Update the repository description and PyPI summary to the same positioning.
- Record baseline stars, forks, downloads, traffic, and referrers.
Status: released
Objective: Let a new user reach a safe unpublished draft without navigating the full README.
- Publish
docs/through GitHub Pages frommain/docs, using GitHub Pages' built-in Markdown and Jekyll support without a third-party documentation framework. - Add a documentation index with paths for:
- Installation and first draft.
- Authentication and cookie handling.
- Unified CLI.
- Python SDK.
- Markdown syntax.
- MCP setup.
- Safety and publishing behavior.
- Troubleshooting.
- Use the existing before and after images in a concrete Markdown-to-Substack walkthrough.
- Add tested examples for:
- Markdown file to unpublished draft.
- Scheduling an existing draft.
- Selecting among multiple publications.
- Stable JSON output in automation.
- Cookie authentication verification.
- Add the GitHub Pages URL to PyPI project links and the repository About section.
- Preserve old documentation paths and provide links where content moves.
- No runtime behavior changes.
- Documentation examples use only supported interfaces.
- Add smoke tests for every executable example.
- Validate internal documentation links and code blocks.
- Verify the Pages site from a clean browser session.
- A new user with credentials can create an unpublished draft by following only the getting-started page.
- Publish a tutorial built with the package itself.
- Share the five-minute workflow with Python and Substack writer communities.
- Compare documentation traffic and PyPI downloads with the 0.2.0 baseline.
The tutorial is stored as unpublished Substack draft 210457101. Public
publication and community sharing are pending explicit approval for the
external account action.
Status: released
Objective: Make MCP useful for inspecting and preparing publications without steering agents toward immediate publishing.
- Add these MCP tools:
get_status()list_publications()list_drafts(filter="draft", offset=0, limit=25)get_draft(draft_id)schedule_draft(draft_id, at)unschedule_draft(draft_id)publish_draft_checked(draft_id, confirm=False, send=False, share_automatically=False)
publish_draft_checkedrejects execution unlessconfirm=True, runs prepublish checks, and defaults to no email delivery.- Keep the existing
publish_drafttool unchanged for compatibility. Document it as the compatibility interface and recommend the checked tool. - Remove duplicated MCP draft-building logic by routing through the existing SDK helper without changing responses.
- Add verified client configuration examples using official documentation current at implementation time.
- Document which MCP tools read, create drafts, perform reversible writes, or publish.
- The seven MCP tools above are additive.
- Existing MCP tools and signatures remain unchanged.
- Unit-test every MCP tool with a fake
Api; the offline suite uses no network. - Verify confirmation rejection, default no-send behavior, ISO timestamp validation, error redaction, and unchanged legacy behavior.
- Smoke-test MCP-extra installation and server startup.
- Run a live MCP smoke test that creates and deletes a disposable draft but never publishes it.
- Publish an MCP workflow demonstration centered on drafting and review.
- Share it with MCP and relevant AI automation communities.
- Measure interest through issue feedback and download movement without adding telemetry.
Status: released
Objective: Remove common operational failures before introducing draft synchronization.
- Resolve
publication_urlby normalized hostname against Substack subdomains and custom domains. - Raise a clear
SubstackRequestExceptionwhen the requested publication is unavailable instead of failing throughNone. - Add optional
timeout=NonetoApi; preserve unlimited waiting when omitted. - Add global CLI
--timeout SECONDS, defaulting to existing behavior when absent. - Pass configured timeouts consistently through SDK requests.
- Detect local image MIME type instead of always encoding uploads as JPEG:
- Preserve JPEG behavior for JPEG files.
- Support PNG, GIF, and WebP.
- Use
application/octet-streamonly when the type cannot be identified.
- Expand secret redaction to cookie-like values in request errors.
- Keep bounded retries restricted to safe GET and DELETE requests.
- Add optional
Api(..., timeout=None). - Add optional global CLI
--timeout. - No existing constructor or CLI invocation changes meaning.
- Cover Substack subdomains, custom domains, unavailable publications, uppercase hosts, and trailing slashes.
- Cover supported image MIME types and unknown extensions.
- Verify timeout omission preserves current request calls.
- Verify POST requests remain non-retried.
- Run live status, draft creation, image upload, scheduling, and cleanup.
- Publish reliability-focused release notes with custom-domain and image-upload examples.
- Use a smaller maintenance announcement instead of a broad campaign.
Status: released
Objective: Give users a safe way to back up a Substack draft before local editing.
-
Add
Api.export_draft_to_markdown(draft_id). -
Return
draft,markdown, andunsupported_nodes. -
Add:
substack drafts export DRAFT_ID -
Add
--output PATHand--force. -
Without
--output, print Markdown to stdout. -
Under global
--json, returnaction,draft_id,markdown, andunsupported_nodes. -
Never overwrite an existing output file without
--force. -
Implement reverse rendering for every Markdown node supported by the forward renderer.
-
Preserve headings, marks, links, lists, images, captions, code language, blockquotes, footnotes, math, pull quotes, and callouts semantically.
-
Emit unsupported Substack nodes as:
<!-- python-substack-node:v1 BASE64URL_JSON --> -
Encode UTF-8 JSON using URL-safe base64 without padding.
-
Do not discard unknown attributes or node types.
-
Document that 0.6 exports opaque nodes but updates containing them become supported in 0.8.
- Add
Api.export_draft_to_markdown(draft_id). - Add
drafts export.
- Add golden tests for every supported node type.
- Export then render preserves semantic content for supported nodes.
- Unsupported-node payloads decode to the original dictionary.
- Malformed draft bodies fail without partial file output.
- Export performs no server writes.
- A live test exports a disposable feature-complete draft and deletes it.
- Publish a "Back up a Substack draft as Markdown" demonstration.
- Emphasize read-only behavior and unsupported-node visibility.
Status: released
Objective: Complete the create, edit locally, inspect, and update loop without risking unsupported content.
-
Add:
Api.update_draft_from_markdown( draft_id, markdown, *, subtitle=None, audience=None, write_comment_permissions=None, search_engine_title=None, search_engine_description=None, slug=None, draft_section_id=None, tags=None, dry_run=False, )
-
Return stable fields:
action,draft_id,dry_run,changed,payload,draft,tags, andunsupported_nodes. -
Update body content by default. Leave metadata unchanged unless explicitly supplied.
-
Treat supplied tags as additive because the SDK does not expose safe tag replacement.
-
dry_run=Truemay fetch the draft but performs no PUT, tag, schedule, publish, or delete request. -
Add:
substack drafts update DRAFT_ID MARKDOWN_FILE -
Support the metadata flags from
drafts create, plus--dry-runand--yes. -
Require interactive confirmation for updates. JSON or noninteractive mode requires
--yes. -
If the remote draft contains unsupported nodes, abort before writing and instruct the user to export it.
-
Do not silently remove, append, or reposition unsupported nodes.
-
Never modify the local Markdown file.
- Add
Api.update_draft_from_markdown. - Add
drafts update. - Existing draft creation remains unchanged.
- Test body-only updates, selective metadata, additive tags, unchanged schedules, no-op updates, dry runs, confirmations, and JSON responses.
- Assert unsupported remote content blocks the PUT.
- Assert post-PUT failures report that the draft may have changed and do not attempt rollback deletion.
- A live test creates, updates, verifies, and deletes a disposable draft without publishing.
- Demonstrate the safe local edit loop.
- Make update safety and dry-run behavior the primary message.
Status: planned
Objective: Allow exported drafts containing Substack-only widgets to be updated without data loss.
- Teach Markdown rendering and updates to recognize
python-substack-node:v1markers. - Validate every marker before writing:
- Valid URL-safe base64.
- Valid UTF-8 JSON.
- Top-level JSON object.
- Non-empty string
type.
- Reinsert valid opaque nodes verbatim at their marker position.
- Compare opaque nodes in the current remote draft with markers in submitted Markdown.
- Abort if remote unsupported nodes are missing, duplicated, stale, or altered.
- Add
allow_unsupported_change=Falseto the Python update API. - Add CLI
--allow-unsupported-change; require it together with--yesfor intentional removal or replacement. - Ordinary HTML comments must never become Substack nodes.
- Cover button, recipe, and Polymarket examples without hard-coding their schemas.
- Update issue #64 against the implemented CLI and close it only after live round-trip verification.
- Add optional
allow_unsupported_change=False. - Add CLI
--allow-unsupported-change. - Version-1 opaque markers are a documented file-format contract through 1.x.
- Exact round trips for arbitrary unknown nodes and nested attributes.
- Reject corrupt, malicious, stale, duplicate, and missing markers.
- Confirm no server request occurs after validation failure.
- A live test verifies that an editor-created unsupported node survives an update unchanged.
- If a live widget cannot be produced safely, keep issue #64 open and do not release 0.8.0.
- Publish a technical preservation demonstration showing before, export, update, and after.
- Target existing users and Markdown-based publishing communities.
Status: planned
Objective: Preserve Substack editor image settings during Markdown updates.
-
Export image metadata immediately after its Markdown image as:
<!-- python-substack-image:v1 BASE64URL_ATTRS_JSON --> -
Markdown owns
src,alt,href, and caption content. -
Preserve all other server and editor image attributes, including unknown future attributes.
-
Always force
isProcessingtofalseduring an update. -
Validate image markers against the current remote image before writing.
-
For unmarked images, preserve editor attributes only when a unique one-to-one exact source URL match exists.
-
If a remote image cannot be matched safely, abort unless explicit image replacement is authorized.
-
Add
allow_image_replacement=Falseto the Python update API. -
Add CLI
--allow-image-replacement, requiring--yes. -
New images without markers retain existing defaults and upload behavior.
- Add optional
allow_image_replacement=False. - Add CLI
--allow-image-replacement. - Version-1 image markers are a documented file-format contract through 1.x.
- Preserve alignment, size, dimensions, watermark URL, offset, top-image state, and unknown attributes.
- Markdown changes to alt, link, caption, or source win over stored values.
- Test duplicate source URLs, stale markers, changed editor uploads, new images, and explicit replacements.
- A live test edits image properties in Substack, performs a Markdown update, and verifies preservation.
- Publish a visual before-and-after image preservation demonstration.
- Update the Markdown support matrix with complete round-trip guarantees.
Status: planned
Objective: Freeze the supported contract and find defects before 1.0.
- Add a complete hand-written public API reference to GitHub Pages.
- Document:
- Supported Python imports and signatures.
- CLI commands, exit codes, and JSON envelopes.
- MCP tools and safety properties.
- Environment variables.
- Opaque Markdown marker formats.
- External Substack API limitations.
- Deprecation and compatibility policy.
- Add snapshot tests for public Python signatures, console entry points, CLI help, JSON keys, and MCP tool schemas.
- Reach at least 85% line coverage while excluding live-only behavior from the offline denominator where appropriate.
- Add Windows and macOS smoke jobs on one supported Python version while retaining the full Linux Python-version matrix.
- Run dependency, build, metadata, documentation-link, and clean-install checks in CI.
- Triage all open issues:
- No unresolved data-loss, authentication, publishing, or packaging defect may remain.
- Lower-priority feature requests may be explicitly deferred.
- Publish a 1.0 migration guide stating that no migration is required from 0.10.0.
- Feature freeze.
- Only additive corrections are allowed.
- Add no broad capability after the release-candidate tag.
- All offline, packaging, MCP, operating-system, and documentation checks pass.
- Live SDK and CLI suites pass twice on separate runs.
- Clean PyPI installations work on the oldest and newest supported Python versions.
- No P0 or P1 issue remains open.
- Release as the public 1.0 candidate and request testing.
- Focus announcements on existing users and contributors.
- Record candidate feedback and the gap to 300 stars and three times baseline downloads.
Status: planned
Objective: Declare the accumulated compatible interface stable and execute the main growth launch.
- Accept only release-blocking fixes discovered during 0.10.0 testing.
- Set version 1.0.0 consistently.
- Change the development classifier to Production/Stable.
- Publish the final compatibility policy:
- No breaking public change before 2.0.
- Legacy console commands remain supported throughout 1.x.
- Deprecations require documentation and warnings before future major-version removal.
- Undocumented upstream Substack behavior remains best-effort and receives compatible patch fixes.
- Finalize the README, Pages documentation, changelog, release notes, and announcement copy.
- Use the package itself to prepare the Substack announcement when configured credentials permit.
- The 0.10.0 interface becomes the 1.x compatibility baseline.
- Make no intentional runtime behavior change in the version-only release.
- Repeat every 0.10.0 gate from a clean checkout.
- Verify Trusted Publishing and artifact attestations.
- Install 1.0.0 from PyPI in clean base and MCP environments.
- Run live status, create, export, update, schedule, unschedule, and cleanup workflows.
- Never publish a disposable live-test draft.
- Publish the GitHub and PyPI release.
- Publish the Substack launch article.
- Post tailored announcements for Python, Substack, MCP, and developer automation communities, including a Show HN submission.
- Lead with a real Markdown-to-draft and update demonstration rather than a feature inventory.
- Include a restrained star request and contribution invitation.
- Record launch-day and 14-day metrics.
- Success requires at least 300 stars and at least three times the 0.2.0 rolling 30-day download baseline.
The roadmap is complete when:
- Every release above is marked
releasedwith its date, tag, metrics, and announcement links. - Existing public behavior remains compatible through 1.0.
- The package has safe create, export, update, widget-preservation, image-preservation, scheduling, publishing, CLI, Python, and MCP workflows.
- GitHub Pages provides task-oriented documentation.
- PyPI publishing uses short-lived OIDC credentials.
- The published 1.0 artifacts pass every clean-install and live validation gate.