copilot-session-usage 0.7.7__tar.gz → 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/skills/gh-release-notes/SKILL.md +21 -4
  2. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/skills/gh-release-notes/scripts/generate_release_notes.py +91 -6
  3. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/workflows/ci.yml +9 -0
  4. copilot_session_usage-0.8.1/.github/workflows/publish.yml +98 -0
  5. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/workflows/release-notes.yml +44 -8
  6. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/workflows/release.yml +76 -16
  7. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/AGENTS.md +2 -2
  8. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/CONSTITUTION.md +2 -2
  9. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/CONTRIBUTING.md +9 -5
  10. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/PKG-INFO +95 -7
  11. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/README.md +94 -6
  12. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/articles/presentation.md +17 -6
  13. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/internal/automated_release_proces.md +55 -32
  14. copilot_session_usage-0.8.1/docs/source/explanation/how-cost-estimation-works.md +309 -0
  15. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/how-to/analyze-specific-session.md +34 -0
  16. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/how-to/batch-and-spending.md +36 -18
  17. copilot_session_usage-0.8.1/docs/source/how-to/copilot-cli-provider.md +146 -0
  18. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/how-to/index.md +1 -0
  19. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/index.md +12 -9
  20. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/tutorials/getting-started.md +29 -0
  21. copilot_session_usage-0.8.1/guidelines.yml +6 -0
  22. copilot_session_usage-0.8.1/knowledge/concepts/copilot-cli.md +51 -0
  23. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/concepts/index.md +1 -1
  24. copilot_session_usage-0.8.1/knowledge/log.md +9 -0
  25. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/skills/copilot-session-usage/SKILL.md +77 -7
  26. copilot_session_usage-0.8.1/skills/copilot-session-usage/references/span-analysis-template.md +105 -0
  27. copilot_session_usage-0.8.1/src/copilot_session_usage/_internal/copilot_cli.py +963 -0
  28. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/_internal/core.py +379 -91
  29. copilot_session_usage-0.8.1/src/copilot_session_usage/api.py +457 -0
  30. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/cli.py +350 -88
  31. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/data/models-and-pricing.lock +3 -3
  32. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/data/models-and-pricing.yml +50 -74
  33. copilot_session_usage-0.8.1/tests/conftest.py +175 -0
  34. copilot_session_usage-0.8.1/tests/test_api.py +587 -0
  35. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_cli.py +371 -7
  36. copilot_session_usage-0.8.1/tests/test_copilot_cli.py +1016 -0
  37. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_core.py +270 -0
  38. copilot_session_usage-0.8.1/tests/test_generate_release_notes.py +333 -0
  39. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_release_notes.py +14 -3
  40. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_rendering.py +157 -0
  41. copilot_session_usage-0.7.7/.github/workflows/publish.yml +0 -35
  42. copilot_session_usage-0.7.7/docs/source/explanation/how-cost-estimation-works.md +0 -167
  43. copilot_session_usage-0.7.7/knowledge/concepts/copilot-cli.md +0 -51
  44. copilot_session_usage-0.7.7/knowledge/log.md +0 -3
  45. copilot_session_usage-0.7.7/src/copilot_session_usage/_internal/copilot_cli.py +0 -18
  46. copilot_session_usage-0.7.7/src/copilot_session_usage/api.py +0 -286
  47. copilot_session_usage-0.7.7/tests/conftest.py +0 -24
  48. copilot_session_usage-0.7.7/tests/test_api.py +0 -256
  49. copilot_session_usage-0.7.7/tests/test_generate_release_notes.py +0 -176
  50. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.editorconfig +0 -0
  51. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.gitattributes +0 -0
  52. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  53. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  54. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/changes/requests/skill-breakdown/01-request.md +0 -0
  55. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/guidelines/git-commit-message.guideline.md +0 -0
  56. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/guidelines/knowledge-base.guidelines.md +0 -0
  57. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/pull_request_template.md +0 -0
  58. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/skills/consolidate-knowledge-base/SKILL.md +0 -0
  59. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/skills/gh-commit-changes/SKILL.md +0 -0
  60. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/skills/gh-create-pull-request/SKILL.md +0 -0
  61. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/skills/record-finding/SKILL.md +0 -0
  62. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.github/workflows/refresh-pricing.yml +0 -0
  63. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.gitignore +0 -0
  64. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/.readthedocs.yaml +0 -0
  65. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/CHANGELOG.md +0 -0
  66. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/LICENSE +0 -0
  67. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/_static/changelog.js +0 -0
  68. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/_static/custom.css +0 -0
  69. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/changelog.md +0 -0
  70. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/conf.py +0 -0
  71. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/explanation/index.md +0 -0
  72. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/how-to/add-commit-trailer.md +0 -0
  73. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/how-to/export-json.md +0 -0
  74. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/how-to/wsl2.md +0 -0
  75. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/installation.md +0 -0
  76. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/reference/api.md +0 -0
  77. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/reference/cli.md +0 -0
  78. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/reference/index.md +0 -0
  79. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/reference/pricing.md +0 -0
  80. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/docs/source/tutorials/index.md +0 -0
  81. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/justfile +0 -0
  82. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Base.schema.yaml +0 -0
  83. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Concept.schema.yaml +0 -0
  84. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Experiment.schema.yaml +0 -0
  85. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Finding.schema.yaml +0 -0
  86. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Hypothesis.schema.yaml +0 -0
  87. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Outcome.schema.yaml +0 -0
  88. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Playbook.schema.yaml +0 -0
  89. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Principle.schema.yaml +0 -0
  90. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Reference.schema.yaml +0 -0
  91. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/_schema/Structure.schema.yaml +0 -0
  92. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/concepts/overview.md +0 -0
  93. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/concepts/session-cost-analysis.md +0 -0
  94. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/concepts/threshold-based-pricing.md +0 -0
  95. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/experiments/index.md +0 -0
  96. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/experiments/verify-subagent-cost-attribution.md +0 -0
  97. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/findings/2026.07.02-00.00-subagent-logs-runsubagent-prefix.md +0 -0
  98. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/findings/2026.07.02-22.00-title-generation-not-counted-as-model-turn.md +0 -0
  99. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/findings/2026.07.02-23.00-cache-write-approximation.md +0 -0
  100. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/findings/2026.07.13-14.27-release-notes-overreported-maintainer-changes.md +0 -0
  101. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/findings/2026.07.24-15.01-actions-token-cannot-create-pr.md +0 -0
  102. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/findings/2026.08.05-16.24-utility-models-appear-in-logs-but-are-not-billed.md +0 -0
  103. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/findings/index.md +0 -0
  104. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/guides/automation-scripts.md +0 -0
  105. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/guides/cost-optimization.md +0 -0
  106. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/guides/index.md +0 -0
  107. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/guides/wsl2-setup.md +0 -0
  108. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/ideas/index.md +0 -0
  109. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/ideas/multi-session-efficiency-analytics.md +0 -0
  110. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/index.md +0 -0
  111. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/principles/findings-are-immutable.md +0 -0
  112. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/principles/index.md +0 -0
  113. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/reference/debug-log-format.md +0 -0
  114. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/reference/index.md +0 -0
  115. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/reference/pricing-formats.md +0 -0
  116. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/structures/cache-cost-approximation.md +0 -0
  117. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/structures/index.md +0 -0
  118. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/structures/knowledge-base-information-types.md +0 -0
  119. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/structures/session-discovery-algorithm.md +0 -0
  120. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/structures/subagent-cost-tracking.md +0 -0
  121. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/knowledge/structures/vscode-copilot-extension.md +0 -0
  122. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/pyproject.toml +0 -0
  123. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/scripts/refresh_pricing.py +0 -0
  124. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/__init__.py +0 -0
  125. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/_internal/__init__.py +0 -0
  126. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/_internal/git.py +0 -0
  127. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/_internal/vscode.py +0 -0
  128. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/data/__init__.py +0 -0
  129. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/src/copilot_session_usage/data/custom-models-pricing.yml +0 -0
  130. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_coverage_gaps.py +0 -0
  131. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_git.py +0 -0
  132. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_pricing_runtime.py +0 -0
  133. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_vscode.py +0 -0
  134. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/tests/test_vscode_platform.py +0 -0
  135. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/uv.lock +0 -0
  136. {copilot_session_usage-0.7.7 → copilot_session_usage-0.8.1}/uv.toml +0 -0
@@ -41,11 +41,15 @@ Markdown.
41
41
  The skill includes `scripts/generate_release_notes.py`, a standalone Python script that:
42
42
 
43
43
  - verifies the requested Git range and Copilot skill availability;
44
+ - writes the deterministic `## Maintenance` output without invoking Copilot when the
45
+ requested Git range contains no commits or the caller explicitly requests
46
+ `--maintenance-only`;
44
47
  - precomputes the commit log and complete diff locally so generation also works
45
48
  when the Copilot CLI cannot inspect Git history inside its tool environment;
46
49
  - invokes the Copilot CLI with `/gh-release-notes`;
47
- - writes the requested output file; and
48
- - verifies only that the requested output file is readable and non-empty. The
50
+ - gives the skill scoped file-writing tools so it writes the final Markdown to the
51
+ requested output file while the response stream is discarded; and
52
+ - verifies that the requested output file follows the release-note output contract. The
49
53
  script does not normalize Markdown or decide user impact, categorize changes,
50
54
  discover documentation, infer breaking changes, or require examples; those
51
55
  decisions belong to this skill.
@@ -59,6 +63,19 @@ python .github/skills/gh-release-notes/scripts/generate_release_notes.py \
59
63
  --output release-notes.md
60
64
  ```
61
65
 
66
+ For an intentional maintenance-only release whose range contains internal commits,
67
+ pass `--maintenance-only` to write the deterministic Maintenance section without
68
+ invoking Copilot.
69
+
70
+ `release-notes.md` is the canonical shared artifact filename. The generator, both
71
+ release workflows, and `gh release --notes-file` use this same file so the release
72
+ body never depends on Copilot's response stream. An explicitly supplied `--output`
73
+ path is honored exactly. For a non-empty range, the generator creates the empty
74
+ handoff file before invoking Copilot so the skill can edit the known
75
+ repository-relative target; the file is accepted only after the skill has replaced
76
+ it with valid Markdown. An empty range produces the valid maintenance output
77
+ directly and does not require Copilot authentication or skill discovery.
78
+
62
79
  The range is Git's two-dot range, `from_ref..to_ref`: `from_ref` itself is excluded
63
80
  and `to_ref` is included. Both refs may be tags, branches, or commit IDs.
64
81
 
@@ -396,12 +413,12 @@ When this skill is invoked by a CI job with an explicit output-file request:
396
413
 
397
414
  - Honor the requested tag range and repository path exactly.
398
415
  - Treat the requested output file as mandatory. Writing it is the only successful completion condition.
399
- - Use the `create` file tool when the requested file does not exist, or the `edit` file tool when it already exists.
416
+ - Replace the pre-created handoff file with the final Markdown using the `edit` file tool.
400
417
  - After writing, use the `read` file tool to verify that the requested file exists and contains the final release-note Markdown.
401
418
  - Do not modify, commit, or push any other repository files.
402
419
  - The output file must contain only the final release-note Markdown, without an explanation, title heading, or code fence.
403
420
  - The first line must be exactly one of: `## New Features`, `## Enhancements`, `## Bug Fixes`, `## Breaking Changes`, `## Examples`, `## Documentation`, or `## Maintenance`.
404
- - Do not write a preamble, title, code fence, or explanatory text before the first release-note section.
421
+ - Do not write a preamble, tool-call transcript, title, code fence, or explanatory text before the first release-note section.
405
422
  - Never use the Copilot response stream as output. The caller may discard it after the file is written.
406
423
  - Do not report the release notes only in the response. If the file cannot be written or verified, the task has failed.
407
424
  - Preserve the user-impact categories, concrete examples, evidence-based breaking-change detection, and repository-derived public documentation links described above.
@@ -13,6 +13,31 @@ from pathlib import Path
13
13
  SKILL_NAME = "gh-release-notes"
14
14
  DEFAULT_OUTPUT = Path("release-notes.md")
15
15
  MAX_GIT_CONTEXT_LENGTH = 60_000
16
+ PERMITTED_HEADINGS = frozenset(
17
+ {
18
+ "## New Features",
19
+ "## Enhancements",
20
+ "## Bug Fixes",
21
+ "## Breaking Changes",
22
+ "## Examples",
23
+ "## Documentation",
24
+ "## Maintenance",
25
+ }
26
+ )
27
+ TRACE_MARKERS = (
28
+ "<function_call",
29
+ "<thinking>",
30
+ "<system_notification>",
31
+ "assistant.reasoning",
32
+ "function_calls",
33
+ "to=bash.exec",
34
+ "to=functions.exec",
35
+ )
36
+ MAINTENANCE_NOTES = (
37
+ "## Maintenance\n\n"
38
+ "This release contains maintenance and internal improvements. "
39
+ "No user-facing behavior changed.\n"
40
+ )
16
41
 
17
42
 
18
43
  def build_parser() -> argparse.ArgumentParser:
@@ -40,6 +65,11 @@ def build_parser() -> argparse.ArgumentParser:
40
65
  default=DEFAULT_OUTPUT,
41
66
  help="Release-note output path (default: release-notes.md).",
42
67
  )
68
+ parser.add_argument(
69
+ "--maintenance-only",
70
+ action="store_true",
71
+ help="Write deterministic maintenance notes without invoking Copilot.",
72
+ )
43
73
  parser.add_argument(
44
74
  "--model",
45
75
  default=None,
@@ -49,7 +79,7 @@ def build_parser() -> argparse.ArgumentParser:
49
79
  "--validate",
50
80
  type=Path,
51
81
  metavar="FILE",
52
- help="Verify that an existing release-note file is readable and non-empty.",
82
+ help="Verify that an existing release-note file follows the output contract.",
53
83
  )
54
84
  return parser
55
85
 
@@ -87,11 +117,20 @@ def build_prompt(
87
117
  git_context: str | None = None,
88
118
  ) -> str:
89
119
  """Build the small orchestration prompt; the skill owns release-note policy."""
120
+ try:
121
+ output_reference = output.relative_to(repo).as_posix()
122
+ except ValueError:
123
+ output_reference = str(output)
124
+
90
125
  prompt = (
91
126
  f"Use the /{SKILL_NAME} skill. Generate release notes for the exact Git range "
92
127
  f"{from_ref}..{to_ref} in {repo}. The skill is authoritative for analysis, "
93
- f"classification, wording, documentation, and Markdown format. Write only "
94
- f"the final release-note Markdown to {output}; do not summarize it in your response. "
128
+ f"classification, wording, documentation, and Markdown format. Write the final "
129
+ f"release-note Markdown directly to the shared repository file "
130
+ f"{output_reference} using the file-writing tools. Its resolved path is {output}. "
131
+ "The release workflow reads this exact file as its notes input; the Copilot "
132
+ "response stream is discarded. Do not put release notes or a summary in your "
133
+ "response, and do not modify any other files. "
95
134
  "Before writing, enforce the skill's final output contract: render every "
96
135
  "documentation URL as concise inline Markdown such as "
97
136
  "See the [pricing reference for details](https://example.com/pricing), never as a "
@@ -130,6 +169,8 @@ def build_copilot_command(
130
169
  "--allow-tool=read",
131
170
  "--allow-tool=write",
132
171
  "--allow-tool=shell(git:*)",
172
+ "--allow-url=https://github.com",
173
+ "--allow-url=https://copilot-session-usage.readthedocs.io",
133
174
  ]
134
175
  if model:
135
176
  command.extend(["--model", model])
@@ -177,6 +218,17 @@ def validate_range(repo: Path, from_ref: str, to_ref: str) -> None:
177
218
  raise ValueError(f"Git ref {from_ref!r} is not an ancestor of {to_ref!r}.")
178
219
 
179
220
 
221
+ def range_has_commits(repo: Path, from_ref: str, to_ref: str) -> bool:
222
+ """Return whether the requested Git range contains at least one commit."""
223
+ count = git_output(repo, "commit count", "rev-list", "--count", f"{from_ref}..{to_ref}")
224
+ return count != "0"
225
+
226
+
227
+ def write_maintenance_notes(output: Path) -> None:
228
+ """Write the deterministic notes required for a maintenance-only release."""
229
+ output.write_text(MAINTENANCE_NOTES, encoding="utf-8")
230
+
231
+
180
232
  def run_skill_check(repo: Path) -> None:
181
233
  """Ensure the release-note skill is installed in the Copilot CLI."""
182
234
  result = subprocess.run(
@@ -204,7 +256,7 @@ def run_copilot(
204
256
  prompt: str,
205
257
  model: str | None,
206
258
  ) -> None:
207
- """Run Copilot CLI and fail with its captured diagnostics when needed."""
259
+ """Run Copilot CLI and leave the generated Markdown in the shared output file."""
208
260
  result = subprocess.run(
209
261
  build_copilot_command(prompt, model),
210
262
  cwd=repo,
@@ -218,13 +270,38 @@ def run_copilot(
218
270
 
219
271
 
220
272
  def validate_output(output: Path) -> None:
221
- """Verify that Copilot created a readable, non-empty output file."""
273
+ """Verify that generated release notes follow the file output contract."""
222
274
  try:
223
275
  content = output.read_text(encoding="utf-8")
224
276
  except OSError as error:
225
277
  raise RuntimeError(f"Unable to read release-note output {output}: {error}") from error
226
278
  if not content.strip():
227
279
  raise RuntimeError(f"Copilot created an empty release-note file: {output}")
280
+
281
+ first_line = content.splitlines()[0] if content.splitlines() else ""
282
+ if first_line not in PERMITTED_HEADINGS:
283
+ raise RuntimeError(
284
+ f"Release-note output must start with one of the permitted section headings: {output}"
285
+ )
286
+
287
+ lowered = content.lower()
288
+ leaked_markers = [marker for marker in TRACE_MARKERS if marker.lower() in lowered]
289
+ if leaked_markers:
290
+ markers = ", ".join(leaked_markers)
291
+ raise RuntimeError(
292
+ f"Release-note output contains Copilot trace markers ({markers}): {output}"
293
+ )
294
+ if "```" in content:
295
+ raise RuntimeError(f"Release-note output must not contain a code fence: {output}")
296
+ if any(line.startswith("# ") for line in content.splitlines()):
297
+ raise RuntimeError(f"Release-note output must not contain a title heading: {output}")
298
+ headings = [line for line in content.splitlines() if line.startswith("## ")]
299
+ invalid_headings = [heading for heading in headings if heading not in PERMITTED_HEADINGS]
300
+ if invalid_headings:
301
+ raise RuntimeError(
302
+ f"Release-note output contains an invalid section heading "
303
+ f"{invalid_headings[0]!r}: {output}"
304
+ )
228
305
  print("Release-note output file verified.")
229
306
 
230
307
 
@@ -234,16 +311,23 @@ def generate_release_notes(
234
311
  to_ref: str,
235
312
  output: Path,
236
313
  model: str | None,
314
+ maintenance_only: bool = False,
237
315
  ) -> None:
238
316
  """Generate release notes, then validate the resulting Markdown file."""
239
317
  repo = repo.resolve()
240
318
  output = resolve_path(output, repo).resolve()
241
319
  validate_range(repo, from_ref, to_ref)
242
320
  output.parent.mkdir(parents=True, exist_ok=True)
243
- output.unlink(missing_ok=True)
321
+ output.write_text("", encoding="utf-8")
244
322
 
245
323
  print(f"Generating release notes from {from_ref} (exclusive) to {to_ref} (inclusive).")
246
324
  print(f"Copilot model: {model or 'CLI default'}")
325
+ if maintenance_only or not range_has_commits(repo, from_ref, to_ref):
326
+ write_maintenance_notes(output)
327
+ validate_output(output)
328
+ print(f"Release notes written to {output}")
329
+ return
330
+
247
331
  require_copilot_token()
248
332
  run_skill_check(repo)
249
333
  git_context = build_git_context(repo, from_ref, to_ref)
@@ -285,6 +369,7 @@ def main(argv: Sequence[str] | None = None) -> int:
285
369
  to_ref=args.to_ref,
286
370
  output=args.output,
287
371
  model=args.model or os.environ.get("COPILOT_MODEL"),
372
+ maintenance_only=args.maintenance_only,
288
373
  )
289
374
  except (OSError, RuntimeError, ValueError) as error:
290
375
  print(f"error: {error}", file=sys.stderr)
@@ -6,6 +6,11 @@ on:
6
6
  pull_request:
7
7
  branches: [main, master]
8
8
  workflow_call:
9
+ inputs:
10
+ ref:
11
+ description: "Commit to validate"
12
+ required: false
13
+ type: string
9
14
 
10
15
  jobs:
11
16
  test:
@@ -17,6 +22,8 @@ jobs:
17
22
 
18
23
  steps:
19
24
  - uses: actions/checkout@v4
25
+ with:
26
+ ref: ${{ inputs.ref || github.sha }}
20
27
 
21
28
  - name: Install uv
22
29
  uses: astral-sh/setup-uv@v5
@@ -49,6 +56,8 @@ jobs:
49
56
 
50
57
  steps:
51
58
  - uses: actions/checkout@v4
59
+ with:
60
+ ref: ${{ inputs.ref || github.sha }}
52
61
 
53
62
  - name: Install uv
54
63
  uses: astral-sh/setup-uv@v5
@@ -0,0 +1,98 @@
1
+ name: Publish existing tag to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ tag:
7
+ description: "Existing version tag to publish, for example v0.7.1"
8
+ required: true
9
+ type: string
10
+
11
+ concurrency:
12
+ group: publish-${{ github.ref }}-${{ inputs.tag }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ publish:
17
+ runs-on: ubuntu-latest
18
+ environment:
19
+ name: pypi
20
+ url: https://pypi.org/p/copilot-session-usage
21
+ permissions:
22
+ contents: read
23
+ id-token: write
24
+
25
+ steps:
26
+ - name: Require the default branch
27
+ env:
28
+ WORKFLOW_REF: ${{ github.ref }}
29
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
30
+ shell: bash
31
+ run: |
32
+ set -euo pipefail
33
+ expected_ref="refs/heads/$DEFAULT_BRANCH"
34
+ if [[ "$WORKFLOW_REF" != "$expected_ref" ]]; then
35
+ echo "::error::PyPI publication must be dispatched from the default branch ($DEFAULT_BRANCH)."
36
+ echo "::error::Received workflow ref: $WORKFLOW_REF"
37
+ exit 1
38
+ fi
39
+
40
+ - name: Check out repository history
41
+ uses: actions/checkout@v4
42
+ with:
43
+ fetch-depth: 0
44
+ ref: ${{ github.sha }}
45
+
46
+ - name: Validate and check out the requested tag
47
+ env:
48
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
49
+ TARGET_TAG: ${{ inputs.tag }}
50
+ shell: bash
51
+ run: |
52
+ set -euo pipefail
53
+
54
+ if [[ ! "$TARGET_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
55
+ echo "::error::Expected a semantic-version tag such as v0.7.1, got '$TARGET_TAG'."
56
+ exit 1
57
+ fi
58
+ if ! git ls-remote --exit-code --refs origin "refs/tags/$TARGET_TAG" >/dev/null; then
59
+ echo "::error::Remote tag $TARGET_TAG does not exist."
60
+ exit 1
61
+ fi
62
+
63
+ git fetch --force origin "refs/tags/$TARGET_TAG:refs/tags/$TARGET_TAG"
64
+ git fetch --force origin \
65
+ "refs/heads/$DEFAULT_BRANCH:refs/remotes/origin/$DEFAULT_BRANCH"
66
+ target_commit="$(git rev-parse --verify "refs/tags/$TARGET_TAG^{commit}")"
67
+ if ! git merge-base --is-ancestor "$target_commit" "refs/remotes/origin/$DEFAULT_BRANCH"; then
68
+ echo "::error::Release tag $TARGET_TAG is not reachable from the default branch."
69
+ exit 1
70
+ fi
71
+ git checkout --detach "$target_commit"
72
+
73
+ - name: Install uv
74
+ uses: astral-sh/setup-uv@v5
75
+
76
+ - name: Build package
77
+ run: uv build
78
+
79
+ - name: Verify package version
80
+ env:
81
+ TARGET_TAG: ${{ inputs.tag }}
82
+ shell: bash
83
+ run: |
84
+ set -euo pipefail
85
+ expected_version="${TARGET_TAG#v}"
86
+ shopt -s nullglob
87
+ artifacts=(
88
+ dist/copilot_session_usage-"$expected_version"-*.whl
89
+ dist/copilot_session_usage-"$expected_version".tar.gz
90
+ )
91
+ if (( ${#artifacts[@]} != 2 )); then
92
+ echo "::error::Built package artifacts do not match $TARGET_TAG."
93
+ printf '%s\n' dist/*
94
+ exit 1
95
+ fi
96
+
97
+ - name: Publish to PyPI
98
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -20,11 +20,52 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
 
22
22
  steps:
23
- - name: Check out tagged source
23
+ - name: Require the default branch
24
+ env:
25
+ WORKFLOW_REF: ${{ github.ref }}
26
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
27
+ shell: bash
28
+ run: |
29
+ set -euo pipefail
30
+ expected_ref="refs/heads/$DEFAULT_BRANCH"
31
+ if [[ "$WORKFLOW_REF" != "$expected_ref" ]]; then
32
+ echo "::error::Release-note recovery must be dispatched from the default branch ($DEFAULT_BRANCH)."
33
+ echo "::error::Received workflow ref: $WORKFLOW_REF"
34
+ exit 1
35
+ fi
36
+
37
+ - name: Check out repository history
24
38
  uses: actions/checkout@v4
25
39
  with:
26
40
  fetch-depth: 0
27
- ref: ${{ inputs.tag || github.ref }}
41
+ ref: ${{ github.sha }}
42
+
43
+ - name: Validate and check out the existing release tag
44
+ env:
45
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
46
+ TARGET_TAG: ${{ inputs.tag }}
47
+ shell: bash
48
+ run: |
49
+ set -euo pipefail
50
+
51
+ if [[ ! "$TARGET_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
52
+ echo "::error::Expected a semantic-version tag such as v0.7.0, got '$TARGET_TAG'."
53
+ exit 1
54
+ fi
55
+ if ! git ls-remote --exit-code --refs origin "refs/tags/$TARGET_TAG" >/dev/null; then
56
+ echo "::error::Remote tag $TARGET_TAG does not exist."
57
+ exit 1
58
+ fi
59
+
60
+ git fetch --force origin "refs/tags/$TARGET_TAG:refs/tags/$TARGET_TAG"
61
+ git fetch --force origin \
62
+ "refs/heads/$DEFAULT_BRANCH:refs/remotes/origin/$DEFAULT_BRANCH"
63
+ target_commit="$(git rev-parse --verify "refs/tags/$TARGET_TAG^{commit}")"
64
+ if ! git merge-base --is-ancestor "$target_commit" "refs/remotes/origin/$DEFAULT_BRANCH"; then
65
+ echo "::error::Release tag $TARGET_TAG is not reachable from the default branch."
66
+ exit 1
67
+ fi
68
+ git checkout --detach "$target_commit"
28
69
 
29
70
  - name: Install uv
30
71
  uses: astral-sh/setup-uv@v5
@@ -36,15 +77,10 @@ jobs:
36
77
  id: range
37
78
  shell: bash
38
79
  env:
39
- TARGET_TAG: ${{ inputs.tag || github.ref_name }}
80
+ TARGET_TAG: ${{ inputs.tag }}
40
81
  run: |
41
82
  set -euo pipefail
42
83
 
43
- if [[ ! "$TARGET_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-].*)?$ ]]; then
44
- echo "::error::Expected a semantic-version tag such as v0.7.0, got '$TARGET_TAG'."
45
- exit 1
46
- fi
47
-
48
84
  previous_tag="$(git tag --sort=-v:refname --merged "$TARGET_TAG" \
49
85
  | awk -v current="$TARGET_TAG" '$0 != current { print; exit }')"
50
86
 
@@ -32,25 +32,44 @@ permissions:
32
32
  contents: write
33
33
 
34
34
  jobs:
35
+ validate-trigger:
36
+ runs-on: ubuntu-latest
37
+
38
+ steps:
39
+ - name: Require the default branch
40
+ env:
41
+ WORKFLOW_REF: ${{ github.ref }}
42
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
43
+ shell: bash
44
+ run: |
45
+ set -euo pipefail
46
+ expected_ref="refs/heads/$DEFAULT_BRANCH"
47
+ if [[ "$WORKFLOW_REF" != "$expected_ref" ]]; then
48
+ echo "::error::Release must be dispatched from the default branch ($DEFAULT_BRANCH)."
49
+ echo "::error::Received workflow ref: $WORKFLOW_REF"
50
+ exit 1
51
+ fi
52
+
35
53
  ci:
54
+ needs: validate-trigger
36
55
  uses: ./.github/workflows/ci.yml
56
+ with:
57
+ ref: ${{ github.sha }}
37
58
 
38
59
  release:
39
- needs: ci
60
+ needs: [validate-trigger, ci]
40
61
  runs-on: ubuntu-latest
41
62
  env:
42
- GH_TOKEN: ${{ github.token }}
43
- RELEASE_BRANCH: ${{ github.event.repository.default_branch }}
44
63
  INCREMENT: ${{ inputs.increment }}
45
64
  DRAFT_RELEASE: ${{ inputs.draft }}
46
65
  FORCE_RELEASE: ${{ inputs.force }}
47
66
 
48
67
  steps:
49
- - name: Check out the default branch
68
+ - name: Check out the validated release commit
50
69
  uses: actions/checkout@v4
51
70
  with:
52
71
  fetch-depth: 0
53
- ref: ${{ github.event.repository.default_branch }}
72
+ ref: ${{ github.sha }}
54
73
 
55
74
  - name: Install uv
56
75
  uses: astral-sh/setup-uv@v5
@@ -70,23 +89,36 @@ jobs:
70
89
  exit 1
71
90
  fi
72
91
 
73
- git switch --force-create "$RELEASE_BRANCH" "origin/$RELEASE_BRANCH"
74
-
75
92
  previous_tag="$(git describe --tags --abbrev=0 HEAD 2>/dev/null || true)"
76
93
  if [[ -z "$previous_tag" ]]; then
77
94
  echo "::error::Could not find a previous release tag."
78
95
  exit 1
79
96
  fi
80
97
 
98
+ maintenance_only=false
81
99
  if [[ "$INCREMENT" == "auto" ]]; then
82
100
  bump_args=(--get-next --yes)
83
- if [[ "$FORCE_RELEASE" == "true" ]]; then
84
- bump_args+=(--allow-no-commit)
85
- fi
86
101
  if ! next_version_output="$(uv run --no-sync cz bump "${bump_args[@]}" 2>&1)"; then
87
- echo "::error::No eligible conventional commits were found for an automatic release. You can either wait for a user-impacting feature or bug fix and start a new release, or enable 'Force a patch release when auto finds no eligible commit' when triggering the 'Release' Action."
88
- printf '%s\n' "$next_version_output"
89
- exit 1
102
+ if grep -q "NO_COMMITS_TO_BUMP" <<<"$next_version_output"; then
103
+ if [[ "$FORCE_RELEASE" != "true" ]]; then
104
+ echo "::error::No eligible conventional commits were found for an automatic release. You can either wait for a user-impacting feature or bug fix and start a new release, or enable 'Force a patch release when auto finds no eligible commit' when triggering the 'Release' Action."
105
+ printf '%s\n' "$next_version_output"
106
+ exit 1
107
+ fi
108
+
109
+ echo "No eligible conventional commits found; forcing a maintenance-only patch release."
110
+ if ! next_version_output="$(uv run --no-sync cz bump --get-next \
111
+ --increment PATCH --yes --allow-no-commit 2>&1)"; then
112
+ echo "::error::Commitizen could not calculate the forced patch release version."
113
+ printf '%s\n' "$next_version_output"
114
+ exit 1
115
+ fi
116
+ maintenance_only=true
117
+ else
118
+ echo "::error::Commitizen could not calculate the requested release version."
119
+ printf '%s\n' "$next_version_output"
120
+ exit 1
121
+ fi
90
122
  fi
91
123
  else
92
124
  echo "Using explicit $INCREMENT bump."
@@ -102,7 +134,7 @@ jobs:
102
134
  fi
103
135
 
104
136
  target_tag="v$next_version"
105
- if [[ ! "$target_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-].*)?$ ]]; then
137
+ if [[ ! "$target_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
106
138
  echo "::error::The calculated version did not create a semantic-version tag."
107
139
  exit 1
108
140
  fi
@@ -115,6 +147,8 @@ jobs:
115
147
 
116
148
  echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
117
149
  echo "target_tag=$target_tag" >> "$GITHUB_OUTPUT"
150
+ echo "target_sha=$(git rev-parse "$target_tag^{commit}")" >> "$GITHUB_OUTPUT"
151
+ echo "maintenance_only=$maintenance_only" >> "$GITHUB_OUTPUT"
118
152
  echo "Prepared local tag $target_tag from $previous_tag without modifying the default branch."
119
153
 
120
154
  - name: Generate release notes
@@ -122,12 +156,18 @@ jobs:
122
156
  COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
123
157
  PREVIOUS_TAG: ${{ steps.version.outputs.previous_tag }}
124
158
  TARGET_TAG: ${{ steps.version.outputs.target_tag }}
159
+ MAINTENANCE_ONLY: ${{ steps.version.outputs.maintenance_only }}
125
160
  run: |
126
161
  set -euo pipefail
162
+ maintenance_args=()
163
+ if [[ "$MAINTENANCE_ONLY" == "true" ]]; then
164
+ maintenance_args+=(--maintenance-only)
165
+ fi
127
166
  uv run -- python .github/skills/gh-release-notes/scripts/generate_release_notes.py \
128
167
  --from-ref "$PREVIOUS_TAG" \
129
168
  --to-ref "$TARGET_TAG" \
130
- --output release-notes.md
169
+ --output release-notes.md \
170
+ "${maintenance_args[@]}"
131
171
 
132
172
  - name: Upload generated notes
133
173
  uses: actions/upload-artifact@v4
@@ -137,6 +177,7 @@ jobs:
137
177
 
138
178
  - name: Create GitHub release
139
179
  env:
180
+ GH_TOKEN: ${{ github.token }}
140
181
  TARGET_TAG: ${{ steps.version.outputs.target_tag }}
141
182
  shell: bash
142
183
  run: |
@@ -168,6 +209,7 @@ jobs:
168
209
 
169
210
  outputs:
170
211
  target_tag: ${{ steps.version.outputs.target_tag }}
212
+ target_sha: ${{ steps.version.outputs.target_sha }}
171
213
 
172
214
  publish:
173
215
  if: ${{ !inputs.draft }}
@@ -185,7 +227,7 @@ jobs:
185
227
  uses: actions/checkout@v4
186
228
  with:
187
229
  fetch-depth: 0
188
- ref: ${{ needs.release.outputs.target_tag }}
230
+ ref: ${{ needs.release.outputs.target_sha }}
189
231
 
190
232
  - name: Install uv
191
233
  uses: astral-sh/setup-uv@v5
@@ -193,5 +235,23 @@ jobs:
193
235
  - name: Build package
194
236
  run: uv build
195
237
 
238
+ - name: Verify package version
239
+ env:
240
+ TARGET_TAG: ${{ needs.release.outputs.target_tag }}
241
+ shell: bash
242
+ run: |
243
+ set -euo pipefail
244
+ expected_version="${TARGET_TAG#v}"
245
+ shopt -s nullglob
246
+ artifacts=(
247
+ dist/copilot_session_usage-"$expected_version"-*.whl
248
+ dist/copilot_session_usage-"$expected_version".tar.gz
249
+ )
250
+ if (( ${#artifacts[@]} != 2 )); then
251
+ echo "::error::Built package artifacts do not match $TARGET_TAG."
252
+ printf '%s\n' dist/*
253
+ exit 1
254
+ fi
255
+
196
256
  - name: Publish to PyPI
197
257
  uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,7 +1,7 @@
1
1
  # AGENTS.md — copilot-session-usage
2
2
 
3
3
  `copilot-session-usage` is a PyPI-installable Python package that extracts usage and
4
- cost analytics from local VS Code Copilot and Copilot CLI session logs.
4
+ cost analytics from local VS Code Copilot, Copilot CLI, and Copilot App session logs.
5
5
 
6
6
  Read [`CONSTITUTION.md`](CONSTITUTION.md) before changing code, tests, packaging,
7
7
  release automation, or knowledge. It contains the durable project rules. This file
@@ -37,7 +37,7 @@ src/copilot_session_usage/
37
37
  ├── cli.py Click CLI entry point
38
38
  ├── _internal/core.py Session parsing, cost analysis, and shaping
39
39
  ├── _internal/vscode.py VS Code workspace discovery
40
- ├── _internal/copilot_cli.py Future Copilot CLI provider stub
40
+ ├── _internal/copilot_cli.py Copilot CLI/App discovery and event parsing
41
41
  └── data/ Bundled model and pricing data
42
42
  tests/ Pytest suite
43
43
  scripts/ Maintenance scripts, including pricing refresh
@@ -24,8 +24,8 @@ Python package, usable from automation, and honest about unavailable evidence.
24
24
  - Public API belongs in `src/copilot_session_usage/api.py`; internal implementation
25
25
  belongs under `src/copilot_session_usage/_internal/`.
26
26
  - Public API functions retain the optional `agent` parameter used for provider
27
- routing. The supported provider values are `vscode` and `cli`; the CLI provider
28
- remains explicitly unsupported until implemented.
27
+ routing. The supported provider values are `vscode`, `cli`, and the explicit
28
+ combined mode `all`.
29
29
  - Keep provider discovery separate from parsing, pricing, shaping, and presentation.
30
30
  - Do not expose internal modules as public API merely to avoid a proper API change.
31
31