@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.
- package/dist/workflow-catalog.js +1 -1
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +25 -1
- package/loops/docs/diagrams.md +13 -4
- package/loops/workflows/agent-release.md +13 -0
- package/loops/workflows/release-tag.yml +47 -4
- package/loops/workflows/release-to-production.yml +50 -0
- package/package.json +1 -1
package/dist/workflow-catalog.js
CHANGED
|
@@ -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
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -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") -->
|
|
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
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
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 }}
|