@plainconceptsplatform/workflows 1.19.0 → 1.20.0

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.
@@ -16,7 +16,7 @@ export const workflowRoutes = [
16
16
  { name: "merge-gate", worker: "agent-merge-gate.md", description: "Decides what happens to the pull request the merge train hands it, the bot's or a person's: repair it, merge it, or hand it to a human.", defaultEnabled: true },
17
17
  { name: "audit", worker: "agent-audit.md", description: "Read-only repository audit. Finds 5-7 problems, scores each 1-10, and files a tracking report plus one story issue per top finding, each sized for Refine.", defaultEnabled: true },
18
18
  { name: "docs-audit", worker: "agent-docs-audit.md", description: "Read-only documentation audit, every fortnight. Checks the documentation against the code and the Platform standard, and files one issue that Refine then sizes and splits, only when something is stale.", defaultEnabled: false },
19
- { name: "release", worker: "agent-release.md", description: "Writes release notes from the commit log, then a deterministic job opens a version-bump pull request with auto-merge; once it merges, release-tag.yml tags the commit and publishes the GitHub Release. Manual dispatch only (operation=release).", defaultEnabled: true, companions: ["release-tag.yml"] },
19
+ { name: "release", worker: "agent-release.md", description: "Writes release notes from the commit log, then a deterministic job opens a version-bump pull request with auto-merge; once it merges, release-tag.yml tags the commit and publishes the GitHub Release. Manual dispatch only (operation=release), or release-to-production.yml, which also deploys the new tag to production.", defaultEnabled: true, companions: ["release-tag.yml", "release-to-production.yml"] },
20
20
  ];
21
21
  /** What an install with no explicit selection gets. An optional worker is added by name. */
22
22
  export const defaultRouteNames = workflowRoutes
@@ -3356,7 +3356,7 @@ for job in authorize classify; do
3356
3356
  sed -n "/^ ${job}:\$/,/^ runs-on:/p" "${WORKFLOWS_DIR}/work-router.yml" | grep -qF "if: vars.AGENTIC_WORKFLOWS_DISABLED == ''" ||
3357
3357
  { OFF_OK=0; echo "FAIL: the router's ${job} job ignores AGENTIC_WORKFLOWS_DISABLED" >&2; }
3358
3358
  done
3359
- for f in "${WORKFLOWS_DIR}/authorize-bot-work.yml" "${WORKFLOWS_DIR}/release-tag.yml"; do
3359
+ for f in "${WORKFLOWS_DIR}/authorize-bot-work.yml" "${WORKFLOWS_DIR}/release-tag.yml" "${WORKFLOWS_DIR}/release-to-production.yml"; do
3360
3360
  [ -f "$f" ] || continue
3361
3361
  grep -qF "vars.AGENTIC_WORKFLOWS_DISABLED == ''" "$f" ||
3362
3362
  { OFF_OK=0; echo "FAIL: $(basename "$f") ignores AGENTIC_WORKFLOWS_DISABLED" >&2; }
@@ -3670,6 +3670,30 @@ fi
3670
3670
  if [ "$GATE_RUN_ID_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
3671
3671
  fi
3672
3672
 
3673
+ echo "── Release to production ──────────────────────────────────────────────────"
3674
+
3675
+ # One button ships a release: release-to-production.yml runs the release worker with deploy=true
3676
+ # (the router's workflow_dispatch is at GitHub's ten-input limit), the release pull request carries
3677
+ # a marker, and release-tag.yml deploys the tag only when it finds it. A release without the marker
3678
+ # must keep not deploying.
3679
+ RTP_YML="${WORKFLOWS_DIR}/release-to-production.yml"
3680
+ RT_YML="${WORKFLOWS_DIR}/release-tag.yml"
3681
+ if [ -f "$RTP_YML" ] && worker_installed release; then
3682
+ RTP_OK=1
3683
+ rtp() { grep -qE "$1" "$2" || { RTP_OK=0; echo "FAIL: release to production: $3" >&2; }; }
3684
+ rtp 'uses: \./\.github/workflows/agent-release\.lock\.yml' "$RTP_YML" 'the button must run the release worker'
3685
+ rtp '^ deploy: "true"$' "$RTP_YML" 'the button must ask the release worker for a deploy'
3686
+ rtp 'default: minor' "$RTP_YML" 'the button must default to a minor bump'
3687
+ rtp '^ permissions: write-all$' "$RTP_YML" 'the button must grant the worker write-all, or the run never starts'
3688
+ if grep -qE '^ deploy:' "$ROUTER_YML"; then RTP_OK=0; echo "FAIL: release to production: the router's workflow_dispatch is at the ten-input limit; deploy belongs to the button" >&2; fi
3689
+ rtp '<!-- release-deploy -->' "${WORKFLOWS_DIR}/agent-release.md" 'the release worker never writes the deploy marker'
3690
+ rtp 'if \[ "\$DEPLOY" = "true" \]' "${WORKFLOWS_DIR}/agent-release.md" 'the deploy marker must depend on the deploy input'
3691
+ rtp "if: steps.release.outputs.deploy == 'true'" "$RT_YML" 'release-tag.yml must deploy only when the release asked for it'
3692
+ rtp 'gh workflow run "\$DEPLOY_WORKFLOW" --repo "\$REPO" --ref "\$TAG"' "$RT_YML" 'release-tag.yml must start the deploy on the tag'
3693
+ rtp '^ DEPLOY_WORKFLOW: ' "$RT_YML" 'release-tag.yml does not declare DEPLOY_WORKFLOW'
3694
+ if [ "$RTP_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
3695
+ fi
3696
+
3673
3697
  echo "── App error privacy ─────────────────────────────────────────────────────"
3674
3698
 
3675
3699
  # The application error report files into the private repository rather than out of it, so
@@ -599,6 +599,7 @@ nothing reaches `main` except through a pull request.
599
599
  ```mermaid
600
600
  flowchart TD
601
601
  relStart("Work Router<br/>release route<br/>operation=release") --> relFacts
602
+ relButton("Release to production button<br/>bump (default minor), deploy=true") --> relFacts
602
603
  relFacts("Facts (rung 3)<br/>Commit log since the last tag<br/>and the current version, to disk") --> relAgent
603
604
  relAgent("Agent (rung 5)<br/>Categorise by conventional-commit<br/>prefix, write release-notes.md.<br/>The only judgement in this route") --> relNoop
604
605
  relNoop("noop<br/>the agent writes a file,<br/>not a GitHub object") --> relConclude
@@ -607,7 +608,11 @@ flowchart TD
607
608
  relMerge{"CI green and<br/>auto-merge allowed?"}
608
609
  relMerge -->|yes| relTag
609
610
  relMerge -.->|auto-merge disabled| relOpen
610
- relTag("release-tag.yml on push to main<br/>GITHUB_TOKEN: tag vX.Y.Z,<br/>Release with the PR body as notes") --> relDone
611
+ relTag("release-tag.yml on push to main<br/>GITHUB_TOKEN: tag vX.Y.Z,<br/>Release with the PR body as notes") --> relDeployQ
612
+ relDeployQ{"Deploy marker<br/>on the release PR?"}
613
+ relDeployQ -->|no| relDone
614
+ relDeployQ -->|yes| relDeploy
615
+ relDeploy(("Deploying<br/>DEPLOY_WORKFLOW started on the tag,<br/>pro reviewers still apply"))
611
616
  relConclude -.->|fails| relFail
612
617
  relOpen(("PR left open<br/>warning names the fix"))
613
618
  relDone(("Released<br/>tag and Release published,<br/>no tag-triggered workflow fired"))
@@ -620,16 +625,20 @@ flowchart TD
620
625
  classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
621
626
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
622
627
  class relStart start
623
- class relFacts,relAgent,relNoop,relPr,relTag action
624
- class relConclude,relMerge decision
628
+ class relButton,relFacts,relAgent,relNoop,relPr,relTag action
629
+ class relConclude,relMerge,relDeployQ decision
625
630
  class relFail failure
626
631
  class relOpen idle
627
- class relDone success
632
+ class relDone,relDeploy success
628
633
  ```
629
634
 
630
635
  The version bump, the tag and the Release are deterministic on purpose. The agent's only output is
631
636
  a file on disk, so a confused model cannot publish a release. The tag and the Release use
632
637
  `GITHUB_TOKEN` so that a consumer deploying on `v*` tags does not deploy because a release was cut.
638
+ The **Release to production** button (`release-to-production.yml`) is the one way a release deploys:
639
+ it runs the worker directly with `deploy=true` (the router's dispatch is at GitHub's ten-input
640
+ limit), the release pull request carries `<!-- release-deploy -->`,
641
+ and only then does `release-tag.yml` start `DEPLOY_WORKFLOW` on the tag, explicitly.
633
642
 
634
643
 
635
644
  ---
@@ -40,6 +40,11 @@ on:
40
40
  required: false
41
41
  type: string
42
42
  default: "auto"
43
+ deploy:
44
+ description: "true when release-to-production.yml asked for a deploy: the release PR then carries the deploy marker"
45
+ required: false
46
+ type: string
47
+ default: "false"
43
48
 
44
49
  runs-on: agents-arc
45
50
  runs-on-slim: agents-arc
@@ -233,10 +238,18 @@ jobs:
233
238
  BASE: ${{ steps.branch.outputs.base }}
234
239
  BRANCH: ${{ steps.branch.outputs.branch }}
235
240
  GH_TOKEN: ${{ steps.app-token.outputs.token }}
241
+ DEPLOY: ${{ inputs.deploy }}
236
242
  run: |
237
243
  set -euo pipefail
244
+ # The deploy marker is what makes release-tag.yml start the production deploy on the
245
+ # new tag. Only release-to-production.yml asks for it; a plain release route never does.
238
246
  {
239
247
  echo "<!-- release:v${NEW_VERSION} -->"
248
+ if [ "$DEPLOY" = "true" ]; then
249
+ echo "<!-- release-deploy -->"
250
+ echo "> Deploys to production once this merges and v${NEW_VERSION} is tagged."
251
+ echo
252
+ fi
240
253
  cat /tmp/release-notes.md
241
254
  } > /tmp/release-pr-body.md
242
255
  url=$(gh pr create \
@@ -19,6 +19,14 @@ on:
19
19
  permissions:
20
20
  contents: write
21
21
  pull-requests: read
22
+ # Starting DEPLOY_WORKFLOW on the tag when the release pull request asked for a deploy.
23
+ actions: write
24
+
25
+ env:
26
+ # The workflow that deploys a `v*` tag to production. Started on the new tag, with GITHUB_TOKEN,
27
+ # only when the release pull request carries the deploy marker (release-to-production.yml).
28
+ DEPLOY_WORKFLOW: app-deploy.yml
29
+ DEPLOY_MARKER: "<!-- release-deploy -->"
22
30
 
23
31
  concurrency:
24
32
  group: release-tag
@@ -38,6 +46,7 @@ jobs:
38
46
  with:
39
47
  persist-credentials: false
40
48
  - name: Tag the release commit and publish the GitHub Release
49
+ id: release
41
50
  env:
42
51
  GH_TOKEN: ${{ github.token }}
43
52
  REPO: ${{ github.repository }}
@@ -80,12 +89,19 @@ jobs:
80
89
  marker="<!-- release:${tag} -->"
81
90
  gh api "repos/$REPO/commits/$SHA/pulls" > /tmp/pulls.json
82
91
  MARKER="$marker" node -e '
83
- const pulls = JSON.parse(require("fs").readFileSync("/tmp/pulls.json", "utf8"));
92
+ const fs = require("fs");
93
+ const pulls = JSON.parse(fs.readFileSync("/tmp/pulls.json", "utf8"));
84
94
  const pr = pulls.find((p) => p.merged_at && (p.body || "").includes(process.env.MARKER));
85
95
  if (!pr) process.exit(3);
86
- const notes = pr.body.split(/\r?\n/).filter((line) => line.trim() !== process.env.MARKER).join("\n").trim();
87
- require("fs").writeFileSync("/tmp/release-notes.md", notes + "\n");
88
- console.log(`Notes from #${pr.number}`);
96
+ const deploy = pr.body.includes(process.env.DEPLOY_MARKER);
97
+ // Neither marker nor the deploy notice belongs in the published release notes.
98
+ const notes = pr.body.split(/\r?\n/)
99
+ .filter((line) => line.trim() !== process.env.MARKER && line.trim() !== process.env.DEPLOY_MARKER)
100
+ .filter((line) => !line.startsWith("> Deploys to production once this merges"))
101
+ .join("\n").trim();
102
+ fs.writeFileSync("/tmp/release-notes.md", notes + "\n");
103
+ fs.writeFileSync("/tmp/release-deploy", deploy ? "true" : "false");
104
+ console.log(`Notes from #${pr.number}${deploy ? ", deploy requested" : ""}`);
89
105
  ' || status=$?
90
106
 
91
107
  if [ "${status:-0}" -eq 3 ]; then
@@ -98,3 +114,30 @@ jobs:
98
114
  gh release create "$tag" --repo "$REPO" --target "$SHA" --title "$tag" --notes-file /tmp/release-notes.md
99
115
  fi
100
116
  echo "Tagged $SHA as $tag and published the GitHub Release."
117
+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
118
+ echo "deploy=$(cat /tmp/release-deploy 2>/dev/null || echo false)" >> "$GITHUB_OUTPUT"
119
+
120
+ # The tag above was created with GITHUB_TOKEN, so its push starts no workflow: a release does
121
+ # not ship by itself. A workflow_dispatch is the exception GitHub allows that token, so a
122
+ # release that asked for a deploy starts DEPLOY_WORKFLOW on the tag explicitly. The deploy
123
+ # workflow's own `pro` environment rules (required reviewers) still apply.
124
+ - name: Deploy the release to production
125
+ if: steps.release.outputs.deploy == 'true'
126
+ env:
127
+ GH_TOKEN: ${{ github.token }}
128
+ REPO: ${{ github.repository }}
129
+ TAG: ${{ steps.release.outputs.tag }}
130
+ shell: bash
131
+ run: |
132
+ set -euo pipefail
133
+ if [ ! -f ".github/workflows/${DEPLOY_WORKFLOW}" ]; then
134
+ echo "::warning::The release asked for a deploy, but .github/workflows/${DEPLOY_WORKFLOW} does not exist; $TAG was tagged and not deployed."
135
+ exit 0
136
+ fi
137
+ gh workflow run "$DEPLOY_WORKFLOW" --repo "$REPO" --ref "$TAG"
138
+ echo "Started $DEPLOY_WORKFLOW on $TAG."
139
+ {
140
+ echo "### $TAG is deploying to production"
141
+ echo
142
+ echo "Follow it in [${DEPLOY_WORKFLOW}](${GITHUB_SERVER_URL}/${REPO}/actions/workflows/${DEPLOY_WORKFLOW})."
143
+ } >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,50 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/release-to-production.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ name: Release to production
3
+ # The one button for a production release, run from the repository's Actions tab (or the badge in
4
+ # its README): Run workflow, then Run workflow again. It runs the release worker with the bump
5
+ # chosen here and deploy=true: the worker opens the `chore(release): vX.Y.Z` pull request carrying a
6
+ # deploy marker, it auto-merges, release-tag.yml tags it, and because of the marker starts
7
+ # DEPLOY_WORKFLOW on the new tag, which ships to production. A `pro` environment with required
8
+ # reviewers still asks a person before the production stage runs.
9
+ #
10
+ # It calls the worker directly, as the router's call-release job does, because the router's
11
+ # workflow_dispatch is at GitHub's limit of ten inputs and has no room for a deploy flag. Running a
12
+ # workflow by hand already requires write access to the repository, which is what the router's
13
+ # authorize job checks. The release route on its own (Main Work Router, operation=release) is
14
+ # unchanged: it tags and publishes a release without deploying anything.
15
+ on:
16
+ workflow_dispatch:
17
+ inputs:
18
+ bump:
19
+ description: Version bump
20
+ required: true
21
+ type: choice
22
+ default: minor
23
+ options:
24
+ - minor
25
+ - patch
26
+ - major
27
+
28
+ permissions:
29
+ contents: read
30
+
31
+ concurrency:
32
+ group: release-to-production
33
+ cancel-in-progress: false
34
+
35
+ jobs:
36
+ release:
37
+ name: Release with a deploy
38
+ # AGENTIC_WORKFLOWS_DISABLED, a repository variable with any value, turns the automation off.
39
+ if: vars.AGENTIC_WORKFLOWS_DISABLED == ''
40
+ uses: ./.github/workflows/agent-release.lock.yml
41
+ # The caller grants what the worker requests; narrower grants make the whole run fail to start.
42
+ permissions: write-all
43
+ with:
44
+ version-bump: ${{ inputs.bump }}
45
+ deploy: "true"
46
+ secrets:
47
+ OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
48
+ # The release PR is pushed and opened with the App token so its CI runs; see the worker's conclude.
49
+ BOT_APP_ID: ${{ secrets.BOT_APP_ID }}
50
+ BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",