shipmill 0.31.0__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 (149) hide show
  1. shipmill-0.31.0/.claude/settings.json +14 -0
  2. shipmill-0.31.0/.claude-plugin/marketplace.json +17 -0
  3. shipmill-0.31.0/.claude-plugin/plugin.json +19 -0
  4. shipmill-0.31.0/.github/shipmill.toml +53 -0
  5. shipmill-0.31.0/.github/workflows/ci.yml +49 -0
  6. shipmill-0.31.0/.github/workflows/deploy.yml +107 -0
  7. shipmill-0.31.0/.github/workflows/land.yml +107 -0
  8. shipmill-0.31.0/.github/workflows/move-major-tag.yml +35 -0
  9. shipmill-0.31.0/.github/workflows/operate-self.yml +44 -0
  10. shipmill-0.31.0/.github/workflows/operate.yml +61 -0
  11. shipmill-0.31.0/.github/workflows/prepare.yml +183 -0
  12. shipmill-0.31.0/.github/workflows/publish.yml +80 -0
  13. shipmill-0.31.0/.github/workflows/release.yml +68 -0
  14. shipmill-0.31.0/.gitignore +8 -0
  15. shipmill-0.31.0/CHANGELOG.md +1000 -0
  16. shipmill-0.31.0/CLAUDE.md +46 -0
  17. shipmill-0.31.0/LICENSE +21 -0
  18. shipmill-0.31.0/PKG-INFO +473 -0
  19. shipmill-0.31.0/README.md +456 -0
  20. shipmill-0.31.0/docs/decisions.md +177 -0
  21. shipmill-0.31.0/docs/design/agent-modes.md +318 -0
  22. shipmill-0.31.0/docs/design/ephemeral-mode.md +321 -0
  23. shipmill-0.31.0/docs/flow.md +115 -0
  24. shipmill-0.31.0/docs/install.md +436 -0
  25. shipmill-0.31.0/docs/postmortems/2026-10-04-pages-rollback-drill.md +41 -0
  26. shipmill-0.31.0/docs/postmortems/TEMPLATE.md +55 -0
  27. shipmill-0.31.0/docs/specs/001-fleet.md +68 -0
  28. shipmill-0.31.0/docs/specs/002-worktree-prune.md +212 -0
  29. shipmill-0.31.0/docs/specs/003-waiting-session.md +217 -0
  30. shipmill-0.31.0/docs/specs/004-agent-app-identity.md +205 -0
  31. shipmill-0.31.0/docs/specs/005-headless-mode.md +404 -0
  32. shipmill-0.31.0/docs/specs/006-app-create.md +233 -0
  33. shipmill-0.31.0/docs/specs/007-app-install.md +123 -0
  34. shipmill-0.31.0/docs/specs/008-status.md +143 -0
  35. shipmill-0.31.0/docs/specs/009-status-picture.md +271 -0
  36. shipmill-0.31.0/docs/specs/010-pypi-publish.md +104 -0
  37. shipmill-0.31.0/docs/specs/TEMPLATE.md +39 -0
  38. shipmill-0.31.0/pyproject.toml +46 -0
  39. shipmill-0.31.0/site/index.html +11 -0
  40. shipmill-0.31.0/skills/github-issue-resolve/SKILL.md +131 -0
  41. shipmill-0.31.0/skills/github-issue-triage/SKILL.md +135 -0
  42. shipmill-0.31.0/skills/github-issue-triage/agents/openai.yaml +4 -0
  43. shipmill-0.31.0/skills/github-issue-triage/references/comments.md +112 -0
  44. shipmill-0.31.0/skills/github-issue-triage/references/design-gate.md +59 -0
  45. shipmill-0.31.0/skills/github-issue-triage/references/implementer-brief.md +34 -0
  46. shipmill-0.31.0/skills/github-issue-triage/references/needs-decision.md +64 -0
  47. shipmill-0.31.0/skills/github-issue-triage/references/spec-gate.md +100 -0
  48. shipmill-0.31.0/skills/github-issue-triage/references/triage-rubric.md +65 -0
  49. shipmill-0.31.0/skills/github-issue-triage/scripts/decisions.py +230 -0
  50. shipmill-0.31.0/skills/github-issue-triage/scripts/specs.py +810 -0
  51. shipmill-0.31.0/skills/github-issue-triage/scripts/triage_state.py +834 -0
  52. shipmill-0.31.0/skills/github-pr-triage/SKILL.md +120 -0
  53. shipmill-0.31.0/skills/github-pr-triage/agents/openai.yaml +4 -0
  54. shipmill-0.31.0/skills/github-pr-triage/references/ci-failures.md +52 -0
  55. shipmill-0.31.0/skills/github-pr-triage/references/landing.md +147 -0
  56. shipmill-0.31.0/skills/github-pr-triage/references/reviewer-brief.md +50 -0
  57. shipmill-0.31.0/skills/github-pr-triage/scripts/changelog_guard.py +347 -0
  58. shipmill-0.31.0/skills/github-pr-triage/scripts/failed_tests.py +97 -0
  59. shipmill-0.31.0/skills/github-pr-triage/scripts/landed.py +76 -0
  60. shipmill-0.31.0/skills/github-pr-triage/scripts/pr_gate.py +104 -0
  61. shipmill-0.31.0/skills/github-pr-triage/scripts/release_ready.py +169 -0
  62. shipmill-0.31.0/skills/github-pr-triage/scripts/shipped.py +107 -0
  63. shipmill-0.31.0/skills/github-ship-watch/SKILL.md +171 -0
  64. shipmill-0.31.0/skills/github-ship-watch/agents/openai.yaml +4 -0
  65. shipmill-0.31.0/skills/github-ship-watch/scripts/fleet.py +417 -0
  66. shipmill-0.31.0/skills/github-ship-watch/scripts/metrics.py +833 -0
  67. shipmill-0.31.0/skills/github-ship-watch/scripts/retro.py +231 -0
  68. shipmill-0.31.0/skills/github-ship-watch/scripts/watch_state.py +1478 -0
  69. shipmill-0.31.0/skills/product-intake/SKILL.md +170 -0
  70. shipmill-0.31.0/skills/product-intake/agents/openai.yaml +4 -0
  71. shipmill-0.31.0/skills/product-intake/references/milestone-proposal.md +40 -0
  72. shipmill-0.31.0/skills/product-intake/references/opportunity.md +66 -0
  73. shipmill-0.31.0/skills/product-intake/scripts/intake_state.py +581 -0
  74. shipmill-0.31.0/skills/product-intake/scripts/roadmap_state.py +345 -0
  75. shipmill-0.31.0/skills/shipmill-setup/SKILL.md +492 -0
  76. shipmill-0.31.0/skills/shipmill-setup/scripts/setup_state.py +343 -0
  77. shipmill-0.31.0/src/shipmill/__init__.py +4 -0
  78. shipmill-0.31.0/src/shipmill/__main__.py +6 -0
  79. shipmill-0.31.0/src/shipmill/agents.py +66 -0
  80. shipmill-0.31.0/src/shipmill/app.py +571 -0
  81. shipmill-0.31.0/src/shipmill/app_create.py +451 -0
  82. shipmill-0.31.0/src/shipmill/app_install.py +180 -0
  83. shipmill-0.31.0/src/shipmill/autonomy.py +176 -0
  84. shipmill-0.31.0/src/shipmill/changelog.py +259 -0
  85. shipmill-0.31.0/src/shipmill/cli.py +784 -0
  86. shipmill-0.31.0/src/shipmill/config.py +106 -0
  87. shipmill-0.31.0/src/shipmill/doctor.py +390 -0
  88. shipmill-0.31.0/src/shipmill/environments.py +106 -0
  89. shipmill-0.31.0/src/shipmill/errors.py +3 -0
  90. shipmill-0.31.0/src/shipmill/gate.py +998 -0
  91. shipmill-0.31.0/src/shipmill/github.py +367 -0
  92. shipmill-0.31.0/src/shipmill/gitrepo.py +125 -0
  93. shipmill-0.31.0/src/shipmill/init.py +299 -0
  94. shipmill-0.31.0/src/shipmill/land.py +254 -0
  95. shipmill-0.31.0/src/shipmill/lanes.py +15 -0
  96. shipmill-0.31.0/src/shipmill/launchd.py +140 -0
  97. shipmill-0.31.0/src/shipmill/notify.py +74 -0
  98. shipmill-0.31.0/src/shipmill/operate.py +857 -0
  99. shipmill-0.31.0/src/shipmill/planner.py +340 -0
  100. shipmill-0.31.0/src/shipmill/policy.py +261 -0
  101. shipmill-0.31.0/src/shipmill/propose.py +158 -0
  102. shipmill-0.31.0/src/shipmill/py.typed +0 -0
  103. shipmill-0.31.0/src/shipmill/roadmap.py +30 -0
  104. shipmill-0.31.0/src/shipmill/schedule.py +85 -0
  105. shipmill-0.31.0/src/shipmill/stamp.py +167 -0
  106. shipmill-0.31.0/src/shipmill/status.py +664 -0
  107. shipmill-0.31.0/src/shipmill/version.py +125 -0
  108. shipmill-0.31.0/src/shipmill/worktrees.py +420 -0
  109. shipmill-0.31.0/tests/__init__.py +0 -0
  110. shipmill-0.31.0/tests/conftest.py +302 -0
  111. shipmill-0.31.0/tests/test_app.py +400 -0
  112. shipmill-0.31.0/tests/test_app_create.py +479 -0
  113. shipmill-0.31.0/tests/test_app_docs.py +91 -0
  114. shipmill-0.31.0/tests/test_app_install.py +248 -0
  115. shipmill-0.31.0/tests/test_app_token.py +443 -0
  116. shipmill-0.31.0/tests/test_autonomy.py +508 -0
  117. shipmill-0.31.0/tests/test_changelog_guard.py +273 -0
  118. shipmill-0.31.0/tests/test_cli_docs.py +201 -0
  119. shipmill-0.31.0/tests/test_config.py +314 -0
  120. shipmill-0.31.0/tests/test_decisions.py +79 -0
  121. shipmill-0.31.0/tests/test_fleet.py +599 -0
  122. shipmill-0.31.0/tests/test_gate.py +584 -0
  123. shipmill-0.31.0/tests/test_gate_app.py +450 -0
  124. shipmill-0.31.0/tests/test_gate_decisions.py +327 -0
  125. shipmill-0.31.0/tests/test_gate_headless.py +713 -0
  126. shipmill-0.31.0/tests/test_gate_prune.py +181 -0
  127. shipmill-0.31.0/tests/test_gate_waiting.py +518 -0
  128. shipmill-0.31.0/tests/test_headless_docs.py +85 -0
  129. shipmill-0.31.0/tests/test_intake_state.py +350 -0
  130. shipmill-0.31.0/tests/test_lanes.py +475 -0
  131. shipmill-0.31.0/tests/test_launchd.py +89 -0
  132. shipmill-0.31.0/tests/test_metrics.py +458 -0
  133. shipmill-0.31.0/tests/test_needs_decision.py +393 -0
  134. shipmill-0.31.0/tests/test_operate.py +847 -0
  135. shipmill-0.31.0/tests/test_publish.py +137 -0
  136. shipmill-0.31.0/tests/test_resolve_skill.py +40 -0
  137. shipmill-0.31.0/tests/test_retro.py +90 -0
  138. shipmill-0.31.0/tests/test_roadmap_state.py +259 -0
  139. shipmill-0.31.0/tests/test_setup.py +436 -0
  140. shipmill-0.31.0/tests/test_setup_state.py +351 -0
  141. shipmill-0.31.0/tests/test_ship_watch.py +978 -0
  142. shipmill-0.31.0/tests/test_specs.py +447 -0
  143. shipmill-0.31.0/tests/test_status.py +179 -0
  144. shipmill-0.31.0/tests/test_status_picture.py +526 -0
  145. shipmill-0.31.0/tests/test_triage_state.py +637 -0
  146. shipmill-0.31.0/tests/test_units.py +361 -0
  147. shipmill-0.31.0/tests/test_workflows.py +42 -0
  148. shipmill-0.31.0/tests/test_worktrees.py +697 -0
  149. shipmill-0.31.0/uv.lock +317 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "extraKnownMarketplaces": {
3
+ "shipmill": {
4
+ "source": {
5
+ "source": "github",
6
+ "repo": "shipmill/shipmill"
7
+ },
8
+ "autoUpdate": true
9
+ }
10
+ },
11
+ "enabledPlugins": {
12
+ "shipmill@shipmill": true
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "shipmill",
3
+ "owner": {
4
+ "name": "Roman Medvedev",
5
+ "email": "pypi@romavm.dev"
6
+ },
7
+ "metadata": {
8
+ "description": "The shipmill plugin: GitHub issue triage, PR landing, and release lanes"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "shipmill",
13
+ "source": "./",
14
+ "description": "Issue to release on GitHub: triage issues, review and land pull requests, and cut releases on lanes from a hand-written CHANGELOG"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "shipmill",
3
+ "version": "0.31.0",
4
+ "description": "Issue to release on GitHub: triage issues, review and land pull requests, and cut releases on lanes from a hand-written CHANGELOG",
5
+ "author": {
6
+ "name": "Roman Medvedev",
7
+ "email": "pypi@romavm.dev"
8
+ },
9
+ "homepage": "https://github.com/shipmill/shipmill",
10
+ "repository": "https://github.com/shipmill/shipmill",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "github",
14
+ "triage",
15
+ "pull-requests",
16
+ "release",
17
+ "changelog"
18
+ ]
19
+ }
@@ -0,0 +1,53 @@
1
+ # shipmill releases itself with its own workflows, at the commit being released
2
+ name = "shipmill"
3
+ mode = "release"
4
+ branch = "main"
5
+ version_files = "pyproject"
6
+ after_stamp = ["uv lock --check"]
7
+
8
+ [changelog]
9
+ style = "keep-a-changelog"
10
+
11
+ [bump]
12
+ from = "headings"
13
+ major = ["Breaking"]
14
+ minor = ["Added", "Changed", "Deprecated", "Removed"]
15
+ patch = ["Fixed", "Security"]
16
+
17
+ [gates]
18
+ blocker_label = "release-blocker"
19
+ blocker_lanes = ["stable"]
20
+
21
+ # A stable release after every batch of merges, and hotfixes by hand
22
+ [lanes.stable]
23
+ quiet_minutes = 30
24
+ github_release = true
25
+ dispatch = ["move-major-tag.yml", "publish.yml"] # publish.yml: PyPI (spec 010)
26
+
27
+ [lanes.hotfix]
28
+ github_release = true
29
+ dispatch = ["publish.yml"]
30
+
31
+ # The plugin's version follows the package's, so a plugin update ships with each release
32
+ [[version_lines]]
33
+ file = ".claude-plugin/plugin.json"
34
+ pattern = '"version": "[^"]+"'
35
+ replace = '"version": "{version}"'
36
+
37
+ # shipmill's own GitHub Pages site: each stable release deploys there, and operate checks that
38
+ # its health.json names the deployed release (#53)
39
+ [environments.github-pages]
40
+ lane = "stable"
41
+ workflow = "deploy.yml"
42
+ health = "https://shipmill.github.io/shipmill/health.json"
43
+
44
+ [operate]
45
+ rollback_after = 3
46
+
47
+ # shipmill's own gate: a background session triages new issues and lands green PRs
48
+ [agents]
49
+ prompt = "/github-issue-triage {repo} triage the new issues; merge when green"
50
+ prs = true
51
+ retry_hours = 24
52
+ app_id = 5209412 # gated sessions write as shipmill-romamo[bot] (spec 004)
53
+ mode = "headless" # what the gate runs; written out, not left to the default (D-20)
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+ workflow_call: # shipmill's Release workflow runs it on its release commit
9
+ inputs:
10
+ ref:
11
+ type: string
12
+ required: true
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ check:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ with:
23
+ ref: ${{ inputs.ref }} # empty outside workflow_call: the event's commit
24
+
25
+ - uses: astral-sh/setup-uv@v10.2.0
26
+ with:
27
+ python-version: "3.14"
28
+
29
+ - run: uv sync --locked
30
+ - run: uv run ruff check src tests skills
31
+ - run: uv run ruff format --check src tests skills
32
+ - run: uv run mypy
33
+ - run: uv run pytest -q
34
+ - name: Check the specs, and that a test proves each criterion of a built one
35
+ run: |
36
+ uv run --no-project python skills/github-issue-triage/scripts/specs.py check
37
+ uv run --no-project python skills/github-issue-triage/scripts/specs.py coverage
38
+ - name: Lint the workflows, including the template init writes
39
+ run: |
40
+ uvx --from actionlint-py actionlint .github/workflows/*.yml
41
+ uv run python -c "
42
+ from shipmill.init import Detected, caller_text
43
+ from shipmill.policy import Style, VersionFiles
44
+ print(caller_text(Detected('demo', 'main', Style.KEEP_A_CHANGELOG, VersionFiles.NONE), 'ci.yml'))
45
+ " > "$RUNNER_TEMP/release.yml"
46
+ uvx --from actionlint-py actionlint -ignore 'ci.yml' "$RUNNER_TEMP/release.yml"
47
+ uv run python -c "from shipmill.init import operate_caller_text; print(operate_caller_text())" \
48
+ > "$RUNNER_TEMP/operate.yml"
49
+ uvx --from actionlint-py actionlint "$RUNNER_TEMP/operate.yml"
@@ -0,0 +1,107 @@
1
+ name: Deploy
2
+
3
+ # shipmill's own deploy target: GitHub Pages at https://shipmill.github.io/shipmill/. land
4
+ # starts it on each stable tag, and shipmill operate on a rollback, with -f tag=<tag>
5
+ # -f environment=github-pages; by hand, run it on the tag too (--ref <tag>), or it fails. The site is site/ at the tag plus a health.json naming the
6
+ # release, {"version": "X.Y.Z"}, which shipmill operate checks against the deployed tag.
7
+
8
+ on:
9
+ workflow_dispatch:
10
+ inputs:
11
+ tag:
12
+ description: "The release tag to deploy, such as v1.2.0"
13
+ type: string
14
+ required: true
15
+ environment:
16
+ description: "The GitHub environment; Pages deploys only to github-pages"
17
+ type: string
18
+ required: true
19
+ simulate-failure:
20
+ # The rollback drill's fault (#53): health.json names a version that isn't the tag, so
21
+ # operate's health check fails and it rolls back, without shipping a broken package.
22
+ # Nothing but the drill sets it
23
+ description: "Rollback drill only: health.json reports a version that isn't the tag"
24
+ type: boolean
25
+ default: false
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ # One Pages deploy at a time, none cancelled: a rollback must not be dropped mid-way
31
+ concurrency:
32
+ group: pages
33
+ cancel-in-progress: false
34
+
35
+ jobs:
36
+ build:
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - name: Check the inputs
40
+ env:
41
+ TAG: ${{ inputs.tag }}
42
+ ENVIRONMENT: ${{ inputs.environment }}
43
+ run: |
44
+ if [ "$ENVIRONMENT" != "github-pages" ]; then
45
+ echo "::error::environment must be github-pages, got '$ENVIRONMENT'"
46
+ exit 1
47
+ fi
48
+ if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.+-]*$ ]]; then
49
+ echo "::error::tag must be a release tag such as v1.2.0, got '$TAG'"
50
+ exit 1
51
+ fi
52
+ # GitHub records the deployment under the run's ref, which operate reads as the tag
53
+ # that runs there (#80): a run started without --ref <tag> would record a branch
54
+ if [ "$GITHUB_REF" != "refs/tags/$TAG" ]; then
55
+ echo "::error::run deploy.yml on the tag it deploys, not $GITHUB_REF:" \
56
+ "gh workflow run deploy.yml --ref $TAG -f tag=$TAG -f environment=$ENVIRONMENT"
57
+ exit 1
58
+ fi
59
+
60
+ - uses: actions/checkout@v7
61
+ with:
62
+ ref: refs/tags/${{ inputs.tag }}
63
+
64
+ - name: Write health.json
65
+ env:
66
+ TAG: ${{ inputs.tag }}
67
+ SIMULATE_FAILURE: ${{ inputs.simulate-failure }}
68
+ run: |
69
+ version="${TAG#v}"
70
+ if [ "$SIMULATE_FAILURE" = "true" ]; then
71
+ version="0.0.0-simulated-failure"
72
+ fi
73
+ printf '{"version": "%s"}\n' "$version" > site/health.json
74
+ cat site/health.json
75
+
76
+ - uses: actions/upload-pages-artifact@v5
77
+ with:
78
+ path: site/
79
+
80
+ deploy:
81
+ needs: build
82
+ runs-on: ubuntu-latest
83
+ permissions:
84
+ pages: write # deploys the artifact
85
+ id-token: write # proves the deploy comes from this workflow
86
+ environment:
87
+ name: github-pages
88
+ url: ${{ steps.deployment.outputs.page_url }}
89
+ steps:
90
+ - id: deployment
91
+ uses: actions/deploy-pages@v5
92
+
93
+ # The first health check right after the deploy, not at the next scheduled operate run
94
+ # (#84); the schedule stays the backstop. A job of its own: a failure here leaves the
95
+ # deployment's success alone. operate deploys a tag to an environment at most once, so a
96
+ # run it starts never deploys this tag again, nor races land's dispatch
97
+ operate:
98
+ needs: deploy
99
+ runs-on: ubuntu-latest
100
+ permissions:
101
+ actions: write # starts operate-self.yml
102
+ steps:
103
+ - name: Start shipmill operate
104
+ env:
105
+ GH_TOKEN: ${{ github.token }}
106
+ GH_REPO: ${{ github.repository }}
107
+ run: gh workflow run operate-self.yml -f dry-run=false
@@ -0,0 +1,107 @@
1
+ name: shipmill land
2
+
3
+ # Called after the caller's CI ran on the release commit. When CI passed it lands the
4
+ # release: main (a stable release on an unmoved main), release/X.Y (a hotfix), or the tag
5
+ # alone (a pre-release, or a stable release promoted while main moved on, which then gets a
6
+ # sync commit on main). It tags, publishes, and starts the lane's dispatch workflows.
7
+ # Whatever happened, the work branch is deleted: a failed release is planned again. Under
8
+ # release = "propose" it then closes the lane's proposal issue.
9
+
10
+ on:
11
+ workflow_call:
12
+ inputs:
13
+ ci-result:
14
+ type: string
15
+ required: true
16
+ lane:
17
+ type: string
18
+ required: true
19
+ version:
20
+ type: string
21
+ required: true
22
+ sha:
23
+ type: string
24
+ required: true
25
+ base:
26
+ type: string
27
+ required: true
28
+ tool:
29
+ type: string
30
+ default: "git+https://github.com/shipmill/shipmill@v0"
31
+
32
+ # No workflow-level permissions: each job sets its own, except close-proposal, which takes
33
+ # the caller's grant, so a caller that grants no `issues: write` still starts this workflow
34
+
35
+ jobs:
36
+ land:
37
+ if: inputs.ci-result == 'success'
38
+ concurrency:
39
+ group: shipmill
40
+ cancel-in-progress: false
41
+ runs-on: ubuntu-latest
42
+ permissions:
43
+ contents: write # main, release branches, tags, and releases
44
+ actions: write # starts dispatch workflows; a push with this token starts none
45
+ steps:
46
+ - uses: actions/checkout@v7
47
+ with:
48
+ ref: ${{ github.event.repository.default_branch }}
49
+ fetch-depth: 0
50
+
51
+ - uses: astral-sh/setup-uv@v10.2.0
52
+
53
+ - name: Land, tag, and publish
54
+ shell: bash # -eo pipefail: a failed land fails the step, though its output goes through tee
55
+ env:
56
+ GH_TOKEN: ${{ github.token }}
57
+ TOOL: ${{ inputs.tool }}
58
+ LANE: ${{ inputs.lane }}
59
+ VERSION: ${{ inputs.version }}
60
+ SHA: ${{ inputs.sha }}
61
+ BASE: ${{ inputs.base }}
62
+ run: |
63
+ uvx --from "$TOOL" shipmill land --lane "$LANE" --version "$VERSION" --sha "$SHA" --base "$BASE" \
64
+ | tee -a "$GITHUB_STEP_SUMMARY"
65
+
66
+ # Closes the lane's "Ready to release" issue, with the tag and this run. It sets no
67
+ # permissions, so it runs with the caller's grant: under release = "propose" closing needs
68
+ # `issues: write` on the caller's land job; without it this job fails, after the release
69
+ # landed. Under any other release autonomy it reads nothing from GitHub
70
+ close-proposal:
71
+ needs: land
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - uses: actions/checkout@v7
75
+ with:
76
+ ref: ${{ github.event.repository.default_branch }}
77
+
78
+ - uses: astral-sh/setup-uv@v10.2.0
79
+
80
+ - name: Close the lane's proposal issue
81
+ shell: bash # -eo pipefail: a failed close fails the step, though its output goes through tee
82
+ env:
83
+ GH_TOKEN: ${{ github.token }}
84
+ TOOL: ${{ inputs.tool }}
85
+ LANE: ${{ inputs.lane }}
86
+ VERSION: ${{ inputs.version }}
87
+ run: |
88
+ uvx --from "$TOOL" shipmill close-proposal --lane "$LANE" --version "$VERSION" | tee -a "$GITHUB_STEP_SUMMARY"
89
+
90
+ cleanup:
91
+ needs: land
92
+ if: always()
93
+ runs-on: ubuntu-latest
94
+ permissions:
95
+ contents: write
96
+ steps:
97
+ - uses: actions/checkout@v7
98
+ with:
99
+ ref: ${{ github.event.repository.default_branch }}
100
+
101
+ - uses: astral-sh/setup-uv@v10.2.0
102
+
103
+ - name: Delete the work branch
104
+ env:
105
+ TOOL: ${{ inputs.tool }}
106
+ VERSION: ${{ inputs.version }}
107
+ run: uvx --from "$TOOL" shipmill cleanup --version "$VERSION"
@@ -0,0 +1,35 @@
1
+ name: Move the major tag
2
+
3
+ # Callers pin `@v0`: after a stable release, point the major tag at it. shipmill's land
4
+ # workflow starts this one with the release's tag.
5
+
6
+ on:
7
+ workflow_dispatch:
8
+ inputs:
9
+ tag:
10
+ description: "The release tag, such as v0.2.0"
11
+ type: string
12
+ required: true
13
+
14
+ permissions:
15
+ contents: write
16
+
17
+ jobs:
18
+ move:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ with:
23
+ fetch-depth: 0
24
+
25
+ - name: Point the major tag at the release
26
+ env:
27
+ TAG: ${{ inputs.tag }}
28
+ run: |
29
+ if ! [[ "$TAG" =~ ^v([0-9]+)\.[0-9]+\.[0-9]+$ ]]; then
30
+ echo "::error::$TAG is not a stable vX.Y.Z tag"
31
+ exit 1
32
+ fi
33
+ major="v${BASH_REMATCH[1]}"
34
+ git tag -f "$major" "$TAG^{commit}"
35
+ git push -f origin "refs/tags/$major"
@@ -0,0 +1,44 @@
1
+ name: Operate
2
+
3
+ # shipmill dogfoods itself: the caller `shipmill init --operate` writes, but calling the
4
+ # reusable operate.yml in this repository (whose name it takes) and running the tool from the
5
+ # checkout. Every 10 minutes shipmill operate checks the health of each environment in
6
+ # .github/shipmill.toml, records it as GitHub deployment statuses, and rolls back an
7
+ # environment that fails its checks, opening an incident issue. Approve a deploy that waits
8
+ # on a proposal issue with: gh workflow run operate-self.yml -f approve=<environment> -f
9
+ # dry-run=false; a proposed rollback with -f approve-rollback=<environment> instead
10
+
11
+ on:
12
+ schedule:
13
+ - cron: "*/10 * * * *"
14
+ workflow_dispatch:
15
+ inputs:
16
+ approve:
17
+ description: "Deploy the tag proposed for this environment, once"
18
+ type: string
19
+ default: ""
20
+ approve-rollback:
21
+ description: "Start the rollback an incident proposes for this environment, once"
22
+ type: string
23
+ default: ""
24
+ dry-run:
25
+ description: "Show what it would do, change nothing"
26
+ type: boolean
27
+ default: true
28
+
29
+ permissions:
30
+ contents: read
31
+
32
+ jobs:
33
+ operate:
34
+ uses: ./.github/workflows/operate.yml
35
+ with:
36
+ approve: ${{ inputs.approve || '' }}
37
+ approve-rollback: ${{ inputs.approve-rollback || '' }}
38
+ dry-run: ${{ inputs.dry-run || false }}
39
+ tool: "."
40
+ permissions:
41
+ contents: read # the config and the release tags
42
+ deployments: write # reads deployments and records health as their statuses
43
+ actions: write # starts the deploy workflows
44
+ issues: write # the hold, the issue a deploy under propose or the hold opens, and incidents
@@ -0,0 +1,61 @@
1
+ name: shipmill operate
2
+
3
+ # Called on a schedule by a repository's operate workflow (see `shipmill init --operate`).
4
+ # It checks the health of each environment in [environments], records it as GitHub
5
+ # deployment statuses, and promotes a release to an environment with `from` once its source
6
+ # baked it (or opens the issue a deploy under propose or a hold waits on). An environment
7
+ # failing [operate] rollback_after checks in a row is rolled back and gets an incident issue.
8
+ # With `approve`, it deploys the tag proposed for that environment, once, instead; with
9
+ # `approve-rollback`, it starts the rollback an incident proposes for it, once.
10
+
11
+ on:
12
+ workflow_call:
13
+ inputs:
14
+ approve:
15
+ description: "An environment whose proposed deploy to run; empty runs the checks"
16
+ type: string
17
+ default: ""
18
+ approve-rollback:
19
+ description: "An environment whose proposed rollback to start; empty runs the checks"
20
+ type: string
21
+ default: ""
22
+ dry-run:
23
+ type: boolean
24
+ default: false
25
+ tool:
26
+ description: "Where uvx installs shipmill from"
27
+ type: string
28
+ default: "git+https://github.com/shipmill/shipmill@v0"
29
+
30
+ # No permissions here or on the job: it runs with the caller's grant, as a reusable job that
31
+ # asks for more than its caller grants fails to start. It needs contents: read, deployments:
32
+ # write, actions: write, and issues: write to open a proposal or an incident issue
33
+
34
+ jobs:
35
+ operate:
36
+ # One operate at a time: a run that overlapped the previous one could deploy twice
37
+ concurrency:
38
+ group: shipmill-operate
39
+ cancel-in-progress: false
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v7
43
+ with:
44
+ ref: ${{ github.event.repository.default_branch }}
45
+ fetch-depth: 0 # the release tags, to tell a missed deploy
46
+
47
+ - uses: astral-sh/setup-uv@v10.2.0
48
+
49
+ - name: Operate
50
+ env:
51
+ GH_TOKEN: ${{ github.token }}
52
+ TOOL: ${{ inputs.tool }}
53
+ APPROVE: ${{ inputs.approve }}
54
+ APPROVE_ROLLBACK: ${{ inputs.approve-rollback }}
55
+ DRY_RUN: ${{ inputs.dry-run && '--dry-run' || '' }}
56
+ run: |
57
+ args=(operate)
58
+ [ -n "$APPROVE" ] && args+=(--approve "$APPROVE")
59
+ [ -n "$APPROVE_ROLLBACK" ] && args+=(--approve-rollback "$APPROVE_ROLLBACK")
60
+ [ -n "$DRY_RUN" ] && args+=(--dry-run)
61
+ uvx --from "$TOOL" shipmill "${args[@]}"