@plainconceptsplatform/workflows 1.11.0 → 1.13.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/README.md +1 -1
- package/dist/index.js +18 -5
- package/dist/template-bundles.js +18 -4
- package/dist/workflow-catalog.d.ts +3 -1
- package/dist/workflow-catalog.js +4 -2
- package/loops/actions/file-audit-issues/action.yml +193 -0
- package/loops/actions/file-audit-issues/audit-issues.cjs +245 -0
- package/loops/actions/issue-size.cjs +36 -0
- package/loops/actions/measure-issue-size/action.yml +47 -0
- package/loops/actions/merge-dependabot/action.yml +54 -0
- package/loops/actions/merge-dependabot/merge-dependabot.mjs +343 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +96 -0
- package/loops/docs/diagrams.md +20 -13
- package/loops/templates/ci/app-ci-dotnet-next.yml +15 -9
- package/loops/templates/ci/app-ci-node-monorepo.yml +5 -1
- package/loops/templates/dependabot-merge/.github/workflows/dependabot-merge.yml +78 -0
- package/loops/templates/platform-template/.github/workflows/platform-template-sync.yml +74 -43
- package/loops/templates/platform-template/scripts/check-platform-conformance.mjs +317 -42
- package/loops/workflows/agent-audit.md +46 -29
- package/loops/workflows/agent-docs-audit.md +44 -18
- package/loops/workflows/agent-refine.md +17 -30
- package/loops/workflows/work-router.yml +19 -0
- package/package.json +1 -1
package/loops/docs/diagrams.md
CHANGED
|
@@ -100,7 +100,7 @@ flowchart LR
|
|
|
100
100
|
gate -.->|"CI failed or conflict"| fix("Fix, push, CI again") --> ci
|
|
101
101
|
gate -.->|".github, secrets<br/>or 10000+ lines"| human(("Human review"))
|
|
102
102
|
review("Someone reviews the PR") --> applyReview("apply-review") --> ci
|
|
103
|
-
audit("audit, weekly") -->|"files
|
|
103
|
+
audit("audit, weekly") -->|"files stories"| refine
|
|
104
104
|
|
|
105
105
|
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
|
|
106
106
|
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
@@ -114,9 +114,7 @@ flowchart LR
|
|
|
114
114
|
class merged success
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
Audit creates work rather than consuming it, and files into `refine` rather than straight to
|
|
118
|
-
`implement`, so a report of several unrelated findings becomes one properly sized issue per finding
|
|
119
|
-
instead of one pull request that has to fix them all.
|
|
117
|
+
Audit creates work rather than consuming it, and files into `refine` rather than straight to `implement`: one story issue per top finding, each under refine's size limit, plus a tracking report refine never sees, instead of one pull request that has to fix them all.
|
|
120
118
|
|
|
121
119
|
### The merge train
|
|
122
120
|
|
|
@@ -489,7 +487,7 @@ before anything changes. Applying them one at a time produces contradictory comm
|
|
|
489
487
|
|
|
490
488
|
## agent-audit.md
|
|
491
489
|
|
|
492
|
-
Creates work rather than consuming it. Read-only: it files
|
|
490
|
+
Creates work rather than consuming it. Read-only: it files a tracking report and one story issue per top finding, and changes nothing else.
|
|
493
491
|
|
|
494
492
|
```mermaid
|
|
495
493
|
flowchart TD
|
|
@@ -506,8 +504,9 @@ flowchart TD
|
|
|
506
504
|
auDedupe{"Already tracked, or<br/>previously rejected?"}
|
|
507
505
|
auDedupe -.->|"all of them"| auQuiet
|
|
508
506
|
auDedupe -->|"some are new"| auFile
|
|
509
|
-
auFile("
|
|
510
|
-
|
|
507
|
+
auFile("Report of every finding,<br/>plus one story per top 3") --> auMeasure
|
|
508
|
+
auMeasure("conclude (rung 6)<br/>each body measured with<br/>refine's rule, trimmed to fit") --> auReport
|
|
509
|
+
auReport(("Filed<br/>report: audit, not refine<br/>stories: refine, sub-issues"))
|
|
511
510
|
auQuiet(("Nothing to file<br/>the right outcome on a<br/>clean codebase"))
|
|
512
511
|
auIdle(("Idle<br/>backlog already full"))
|
|
513
512
|
|
|
@@ -517,12 +516,14 @@ flowchart TD
|
|
|
517
516
|
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
|
|
518
517
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
519
518
|
class auStart start
|
|
520
|
-
class auFacts,auRun,auScore,auFile action
|
|
519
|
+
class auFacts,auRun,auScore,auFile,auMeasure action
|
|
521
520
|
class auBack,auFilter,auDedupe decision
|
|
522
521
|
class auIdle idle
|
|
523
522
|
class auReport,auQuiet success
|
|
524
523
|
```
|
|
525
524
|
|
|
525
|
+
The report never goes to refine: refine's size gate refused every audit filed as one issue of every finding and three stories, from 2026-09-21 until 1.12.1. Each story is filed on its own, labelled `refine`, linked under the report and listed in it, so `audit-close` closes the report once its stories are closed. A story already tracked by an open issue is listed against that issue instead of filed again.
|
|
526
|
+
|
|
526
527
|
Memory matters here: open issues only cover what is still open, so a finding reported weeks ago and
|
|
527
528
|
consciously not acted on would come back every single run. The audit records its dispositions and
|
|
528
529
|
reads them next time.
|
|
@@ -549,8 +550,9 @@ flowchart TD
|
|
|
549
550
|
daDedupe{"New finding, proven<br/>on both sides?"}
|
|
550
551
|
daDedupe -.->|"none left"| daQuiet
|
|
551
552
|
daDedupe -->|"some"| daFile
|
|
552
|
-
daFile("
|
|
553
|
-
|
|
553
|
+
daFile("Report of every finding,<br/>plus one story per top 3") --> daMeasure
|
|
554
|
+
daMeasure("conclude (rung 6)<br/>each body measured with<br/>refine's rule, trimmed to fit") --> daReport
|
|
555
|
+
daReport(("Filed<br/>report: docs-audit, not refine<br/>stories: refine, sub-issues"))
|
|
554
556
|
daQuiet(("Nothing to file<br/>the documentation is current"))
|
|
555
557
|
daIdle(("Idle<br/>no agent, no spend"))
|
|
556
558
|
|
|
@@ -560,7 +562,7 @@ flowchart TD
|
|
|
560
562
|
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
|
|
561
563
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
562
564
|
class daStart start
|
|
563
|
-
class daFacts,daRun,daFile action
|
|
565
|
+
class daFacts,daRun,daFile,daMeasure action
|
|
564
566
|
class daBack,daMoved,daDedupe decision
|
|
565
567
|
class daIdle idle
|
|
566
568
|
class daReport,daQuiet success
|
|
@@ -569,6 +571,8 @@ flowchart TD
|
|
|
569
571
|
Reports join the audit chain: `audit-close` closes one when its children are closed, and marks it
|
|
570
572
|
`stale-audit` when nobody acted on it, which frees the slot.
|
|
571
573
|
|
|
574
|
+
It files the way the audit does, through the same `file-audit-issues` step: the report never goes to refine, which refused a single report of every finding once it grew past its size gate. Each top story is filed on its own, labelled `refine`, linked under the report and listed in it.
|
|
575
|
+
|
|
572
576
|
---
|
|
573
577
|
|
|
574
578
|
## agent-release.md
|
|
@@ -652,7 +656,8 @@ flowchart TB
|
|
|
652
656
|
hkBranch -->|yes| hkDelete("Delete the branch")
|
|
653
657
|
hkBranch -.->|"open PR, human PR,<br/>or the default branch"| hkDigest
|
|
654
658
|
hkDelete --> hkDigest
|
|
655
|
-
hkDigest("Rewrite one issue:<br/>Needs a human") -->
|
|
659
|
+
hkDigest("Rewrite one issue:<br/>Needs a human") --> hkDependabot
|
|
660
|
+
hkDependabot("Squash-merge Dependabot PRs<br/>whose checks are all green") --> hkEnd
|
|
656
661
|
hkEnd(("One place to look<br/>instead of four repos"))
|
|
657
662
|
|
|
658
663
|
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
@@ -660,7 +665,7 @@ flowchart TB
|
|
|
660
665
|
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
661
666
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
662
667
|
class hkCron start
|
|
663
|
-
class hkScan,hkRetry,hkStrand,hkParent,hkDelete,hkDigest,hkStackClose action
|
|
668
|
+
class hkScan,hkRetry,hkStrand,hkParent,hkDelete,hkDigest,hkStackClose,hkDependabot action
|
|
664
669
|
class hkPark,hkWhy,hkBudget,hkBranch,hkStack,hkStackWait decision
|
|
665
670
|
class hkEnd success
|
|
666
671
|
```
|
|
@@ -681,6 +686,8 @@ held for days after its announcement, a root closed by accident with children op
|
|
|
681
686
|
root pull request each get one line in the digest. No labels move, so nothing the router listens
|
|
682
687
|
for is raised by the sweep itself.
|
|
683
688
|
|
|
689
|
+
The last step merges Dependabot's updates through the `merge-dependabot` action: only pull requests Dependabot opened with every commit verified and its own, squash-merged once every check is green, a stale one asked once to rebase, a failing one listed in the job summary. It reads nothing of the pull request but the API, and `DEPENDABOT_MERGE` sets its scope. See `docs/dependabot-merge.md` in the package.
|
|
690
|
+
|
|
684
691
|
---
|
|
685
692
|
|
|
686
693
|
## agentics-app-errors
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# engineering-excellence-{coverage,complexity,security,sbom,hygiene} facets Project-Health
|
|
10
10
|
# collects. The facets upload from main only, which this workflow reaches through `App: Deploy` on
|
|
11
11
|
# every push and through its own weekly schedule. The platform-conformance job runs the
|
|
12
|
-
# platform-template guard where the app
|
|
12
|
+
# platform-template guard where the app declares a platform contract.
|
|
13
13
|
#
|
|
14
14
|
# Everything that differs between apps is a top-level `env:` value below or a floor in
|
|
15
15
|
# quality.config.json. Job names are required status checks on main: do not rename them.
|
|
@@ -546,8 +546,10 @@ jobs:
|
|
|
546
546
|
if: env.REPO_CHECK_COMMANDS != ''
|
|
547
547
|
run: bash -euo pipefail -c "$REPO_CHECK_COMMANDS"
|
|
548
548
|
|
|
549
|
-
#
|
|
550
|
-
# platform-template template; a repository
|
|
549
|
+
# Paths a platform contract (.platform/contract.yml) marks as owned change only through a template
|
|
550
|
+
# sync. The guard comes with the platform-template template; a repository with no contract (nor
|
|
551
|
+
# .copier-answers.yml with .platform/manifest, read as one) has not adopted it and passes. The
|
|
552
|
+
# owned files are also compared with the template at the recorded version wherever git can read it.
|
|
551
553
|
platform-conformance:
|
|
552
554
|
name: Platform conformance
|
|
553
555
|
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
@@ -559,25 +561,25 @@ jobs:
|
|
|
559
561
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
560
562
|
with:
|
|
561
563
|
node-version: ${{ env.NODE_VERSION }}
|
|
562
|
-
- name:
|
|
564
|
+
- name: Owned files change only through a template sync
|
|
563
565
|
env:
|
|
564
566
|
EVENT: ${{ github.event_name }}
|
|
565
567
|
BASE_REF: ${{ github.base_ref }}
|
|
566
568
|
run: |
|
|
567
569
|
set -euo pipefail
|
|
568
|
-
if [ ! -f .copier-answers.yml ]; then
|
|
569
|
-
echo "No .
|
|
570
|
+
if [ ! -f .platform/contract.yml ] && { [ ! -f .copier-answers.yml ] || [ ! -f .platform/manifest ]; }; then
|
|
571
|
+
echo "No .platform/contract.yml: this repository declares no platform contract."
|
|
570
572
|
exit 0
|
|
571
573
|
fi
|
|
572
574
|
if [ ! -f scripts/check-platform-conformance.mjs ]; then
|
|
573
|
-
echo "::error
|
|
575
|
+
echo "::error::This repository has a platform contract but no scripts/check-platform-conformance.mjs. Install it with: npx @plainconceptsplatform/workflows add --template platform-template"
|
|
574
576
|
exit 1
|
|
575
577
|
fi
|
|
576
578
|
if [ "$EVENT" = pull_request ]; then
|
|
577
579
|
git fetch origin "$BASE_REF"
|
|
578
|
-
node scripts/check-platform-conformance.mjs --base "origin/$BASE_REF" | tee -a "$GITHUB_STEP_SUMMARY"
|
|
580
|
+
node scripts/check-platform-conformance.mjs --compare --base "origin/$BASE_REF" | tee -a "$GITHUB_STEP_SUMMARY"
|
|
579
581
|
else
|
|
580
|
-
node scripts/check-platform-conformance.mjs | tee -a "$GITHUB_STEP_SUMMARY"
|
|
582
|
+
node scripts/check-platform-conformance.mjs --compare | tee -a "$GITHUB_STEP_SUMMARY"
|
|
581
583
|
fi
|
|
582
584
|
|
|
583
585
|
mutation-web:
|
|
@@ -961,6 +963,9 @@ jobs:
|
|
|
961
963
|
# If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
|
|
962
964
|
# `actions: write`. A called workflow may not request a scope its caller withheld, and the
|
|
963
965
|
# whole run fails to start with zero jobs and no annotation when it does.
|
|
966
|
+
#
|
|
967
|
+
# Not for Dependabot: its runs get a read-only token, so the dispatch would fail its pull request,
|
|
968
|
+
# and the router's maintenance run merges Dependabot updates instead of the train.
|
|
964
969
|
dispatch-merge-gate:
|
|
965
970
|
name: Dispatch merge gate
|
|
966
971
|
needs: [api, mutation-api, web, mutation-web, secret-scan, deps-and-iac-scan, sast-scan, sbom, engineering-excellence, platform-conformance, full-ci]
|
|
@@ -970,6 +975,7 @@ jobs:
|
|
|
970
975
|
github.event_name == 'pull_request' &&
|
|
971
976
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
972
977
|
github.event.pull_request.draft == false &&
|
|
978
|
+
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
|
973
979
|
(github.event.pull_request.user.login == 'app/github-actions' || endsWith(github.event.pull_request.user.login, '[bot]'))
|
|
974
980
|
runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
|
|
975
981
|
timeout-minutes: 5
|
|
@@ -239,6 +239,9 @@ jobs:
|
|
|
239
239
|
# If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
|
|
240
240
|
# `actions: write`. A called workflow may not request a scope its caller withheld, and the
|
|
241
241
|
# whole run fails to start with zero jobs and no annotation when it does.
|
|
242
|
+
#
|
|
243
|
+
# Not for Dependabot: its runs get a read-only token, so the dispatch would fail its pull request,
|
|
244
|
+
# and the router's maintenance run merges Dependabot updates instead of the train.
|
|
242
245
|
dispatch-merge-train:
|
|
243
246
|
name: Dispatch merge train
|
|
244
247
|
needs: [lint, web, mutation-web, e2e, desktop, mobile]
|
|
@@ -246,7 +249,8 @@ jobs:
|
|
|
246
249
|
always() &&
|
|
247
250
|
github.event_name == 'pull_request' &&
|
|
248
251
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
249
|
-
github.event.pull_request.draft == false
|
|
252
|
+
github.event.pull_request.draft == false &&
|
|
253
|
+
github.event.pull_request.user.login != 'dependabot[bot]'
|
|
250
254
|
runs-on: agents-arc
|
|
251
255
|
timeout-minutes: 5
|
|
252
256
|
permissions:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/dependabot-merge/.github/workflows/dependabot-merge.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: "Dependabot: Merge"
|
|
3
|
+
|
|
4
|
+
# Every six hours, squash-merges the open Dependabot pull requests whose checks are all green, for a
|
|
5
|
+
# repository that does not run the work router (the router does the same in its maintenance run).
|
|
6
|
+
# Plain YAML, no model, and the pull request's code is never checked out: the checkout below is the
|
|
7
|
+
# default branch, for the action. Only pull requests Dependabot opened, with every commit
|
|
8
|
+
# Dependabot's, are touched. See docs/dependabot-merge.md in agentic-workflows.
|
|
9
|
+
#
|
|
10
|
+
# With the Platform App secrets (BOT_APP_ID / BOT_PRIVATE_KEY) the merge runs as the App, and the
|
|
11
|
+
# merge commit starts the default branch's push workflows. Without them it runs as GITHUB_TOKEN,
|
|
12
|
+
# and GitHub starts no workflow from a merge made with that token: nothing deploys on push until
|
|
13
|
+
# the next commit that is not a Dependabot merge.
|
|
14
|
+
#
|
|
15
|
+
# To turn it off: set DEPENDABOT_MERGE to "off" below, or the repository variable DEPENDABOT_MERGE
|
|
16
|
+
# to "off", or AGENTIC_WORKFLOWS_DISABLED to anything, or delete this file.
|
|
17
|
+
on:
|
|
18
|
+
schedule:
|
|
19
|
+
- cron: "41 */6 * * *"
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
inputs:
|
|
22
|
+
dry-run:
|
|
23
|
+
description: "Decide and report, merge nothing"
|
|
24
|
+
required: false
|
|
25
|
+
type: boolean
|
|
26
|
+
default: false
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
concurrency:
|
|
32
|
+
group: dependabot-merge
|
|
33
|
+
cancel-in-progress: false
|
|
34
|
+
|
|
35
|
+
env:
|
|
36
|
+
# "all", "minor" (minor and patch), "patch", or "off". CI is the gate.
|
|
37
|
+
DEPENDABOT_MERGE: "all"
|
|
38
|
+
# "true" also merges in a repository with no checks at all, once the head is an hour old.
|
|
39
|
+
DEPENDABOT_MERGE_WITHOUT_CHECKS: "true"
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
merge:
|
|
43
|
+
name: Merge Dependabot pull requests
|
|
44
|
+
if: "!github.event.repository.fork && vars.AGENTIC_WORKFLOWS_DISABLED == ''"
|
|
45
|
+
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
46
|
+
timeout-minutes: 15
|
|
47
|
+
permissions:
|
|
48
|
+
# Merging, deleting the merged branch, approving where a ruleset asks for it, and commenting
|
|
49
|
+
# `@dependabot rebase`. All of it through the API.
|
|
50
|
+
contents: write
|
|
51
|
+
pull-requests: write
|
|
52
|
+
issues: write
|
|
53
|
+
checks: read
|
|
54
|
+
statuses: read
|
|
55
|
+
env:
|
|
56
|
+
HAS_APP: ${{ secrets.BOT_APP_ID != '' && secrets.BOT_PRIVATE_KEY != '' }}
|
|
57
|
+
steps:
|
|
58
|
+
- name: Checkout the action
|
|
59
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
60
|
+
with:
|
|
61
|
+
persist-credentials: false
|
|
62
|
+
sparse-checkout: .github/actions/merge-dependabot
|
|
63
|
+
|
|
64
|
+
- name: Generate App token
|
|
65
|
+
id: app-token
|
|
66
|
+
if: env.HAS_APP == 'true'
|
|
67
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
68
|
+
with:
|
|
69
|
+
app-id: ${{ secrets.BOT_APP_ID }}
|
|
70
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
71
|
+
|
|
72
|
+
- uses: ./.github/actions/merge-dependabot
|
|
73
|
+
with:
|
|
74
|
+
token: ${{ steps.app-token.outputs.token || github.token }}
|
|
75
|
+
scope: ${{ vars.DEPENDABOT_MERGE || env.DEPENDABOT_MERGE }}
|
|
76
|
+
merge-without-checks: ${{ env.DEPENDABOT_MERGE_WITHOUT_CHECKS }}
|
|
77
|
+
post-merge-event: ""
|
|
78
|
+
dry-run: ${{ github.event.inputs.dry-run || 'false' }}
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/platform-template/.github/workflows/platform-template-sync.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
2
|
name: "Platform: Template sync"
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Pulls the newest version of the upstream template named in .platform/contract.yml (see
|
|
5
|
+
# docs/platform-contracts.md in agentic-workflows). Plain YAML, no model. Without a contract it does
|
|
6
|
+
# nothing. With one it runs `sync.update` and opens or updates one pull request from
|
|
7
|
+
# `platform/template-sync`. copier is installed when the command starts with `copier`; any other
|
|
8
|
+
# command is the repository's to provide. Conflict markers left in an owned file fail the
|
|
9
|
+
# conformance guard, so the merge gate repairs the pull request like any red one.
|
|
6
10
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# `_src_path`. No change means the app is current and the run ends there. Otherwise the result is
|
|
10
|
-
# committed to `platform/sync-<tag>` and opened as a pull request titled with the tag. A file the
|
|
11
|
-
# app had edited gets copier's inline conflict markers; scripts/check-platform-conformance.mjs
|
|
12
|
-
# fails CI on them, so the merge gate repairs the pull request (`remediated`) like any red one.
|
|
13
|
-
#
|
|
14
|
-
# Kitchen-Sink is private, so the Platform App token (BOT_APP_ID / BOT_PRIVATE_KEY) reads it. The
|
|
15
|
-
# same token pushes the branch and opens the pull request, because a push made with GITHUB_TOKEN
|
|
16
|
-
# starts no workflow and CI would never run on the sync.
|
|
11
|
+
# The Platform App token (BOT_APP_ID / BOT_PRIVATE_KEY) reads a private template and pushes, because
|
|
12
|
+
# a push made with GITHUB_TOKEN starts no workflow and CI would never run on the sync.
|
|
17
13
|
on:
|
|
18
14
|
schedule:
|
|
19
15
|
- cron: "17 5 * * 1"
|
|
@@ -29,6 +25,8 @@ concurrency:
|
|
|
29
25
|
env:
|
|
30
26
|
# copier 9.19.1 needs Python 3.10 or newer on the runner.
|
|
31
27
|
COPIER_VERSION: "9.19.1"
|
|
28
|
+
NODE_VERSION: "22"
|
|
29
|
+
SYNC_BRANCH: platform/template-sync
|
|
32
30
|
|
|
33
31
|
jobs:
|
|
34
32
|
sync:
|
|
@@ -41,19 +39,40 @@ jobs:
|
|
|
41
39
|
fetch-depth: 0
|
|
42
40
|
persist-credentials: false
|
|
43
41
|
|
|
44
|
-
-
|
|
45
|
-
|
|
42
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
43
|
+
with:
|
|
44
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
45
|
+
|
|
46
|
+
# An invalid contract fails here, with the file and the field in the error.
|
|
47
|
+
- name: Read the platform contract
|
|
48
|
+
id: contract
|
|
49
|
+
run: |
|
|
50
|
+
set -euo pipefail
|
|
51
|
+
node scripts/check-platform-conformance.mjs --contract | tee -a "$GITHUB_OUTPUT"
|
|
52
|
+
|
|
53
|
+
- name: Decide whether to sync
|
|
54
|
+
id: decide
|
|
55
|
+
env:
|
|
56
|
+
CONTRACT: ${{ steps.contract.outputs.contract }}
|
|
57
|
+
SELF: ${{ steps.contract.outputs.self }}
|
|
58
|
+
UPDATE: ${{ steps.contract.outputs.update }}
|
|
46
59
|
run: |
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
set -euo pipefail
|
|
61
|
+
run=false
|
|
62
|
+
if [ "$CONTRACT" != present ]; then
|
|
63
|
+
echo "No .platform/contract.yml (nor .copier-answers.yml with .platform/manifest): nothing to sync."
|
|
64
|
+
elif [ "$SELF" = true ]; then
|
|
65
|
+
echo "This repository is the template source.url names: nothing to sync."
|
|
66
|
+
elif [ -z "$UPDATE" ]; then
|
|
67
|
+
echo "The platform contract declares no sync.update: nothing to sync."
|
|
49
68
|
else
|
|
50
|
-
|
|
51
|
-
echo "No .copier-answers.yml: this repository has not adopted the platform template. Nothing to do."
|
|
69
|
+
run=true
|
|
52
70
|
fi
|
|
71
|
+
echo "run=$run" >> "$GITHUB_OUTPUT"
|
|
53
72
|
|
|
54
73
|
- name: Generate App token
|
|
55
74
|
id: app-token
|
|
56
|
-
if: steps.
|
|
75
|
+
if: steps.decide.outputs.run == 'true'
|
|
57
76
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
58
77
|
with:
|
|
59
78
|
app-id: ${{ secrets.BOT_APP_ID }}
|
|
@@ -62,7 +81,7 @@ jobs:
|
|
|
62
81
|
|
|
63
82
|
# uvx where uv is installed, then pipx, then a virtual environment from the runner's Python.
|
|
64
83
|
- name: Install copier
|
|
65
|
-
if: steps.
|
|
84
|
+
if: steps.decide.outputs.run == 'true' && startsWith(steps.contract.outputs.update, 'copier ')
|
|
66
85
|
run: |
|
|
67
86
|
set -euo pipefail
|
|
68
87
|
bin="$RUNNER_TEMP/copier-bin"
|
|
@@ -87,8 +106,10 @@ jobs:
|
|
|
87
106
|
# The token reaches git through process-scoped config only, never a file on the runner.
|
|
88
107
|
- name: Update from the template
|
|
89
108
|
id: update
|
|
90
|
-
if: steps.
|
|
109
|
+
if: steps.decide.outputs.run == 'true'
|
|
91
110
|
env:
|
|
111
|
+
UPDATE: ${{ steps.contract.outputs.update }}
|
|
112
|
+
BEFORE: ${{ steps.contract.outputs.version }}
|
|
92
113
|
GIT_CONFIG_COUNT: "1"
|
|
93
114
|
GIT_CONFIG_KEY_0: url.https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/.insteadOf
|
|
94
115
|
GIT_CONFIG_VALUE_0: https://github.com/
|
|
@@ -98,21 +119,19 @@ jobs:
|
|
|
98
119
|
GIT_COMMITTER_EMAIL: ${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com
|
|
99
120
|
run: |
|
|
100
121
|
set -euo pipefail
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
copier update --trust --defaults --conflict inline
|
|
104
|
-
after=$(commit_of)
|
|
122
|
+
echo "sync.update: $UPDATE"
|
|
123
|
+
bash -euo pipefail -c "$UPDATE"
|
|
105
124
|
if [ -z "$(git status --porcelain)" ]; then
|
|
106
|
-
echo "Already on the newest template ($
|
|
125
|
+
echo "Already on the newest template (${BEFORE:-no version recorded}). Nothing to do."
|
|
107
126
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
108
127
|
exit 0
|
|
109
128
|
fi
|
|
129
|
+
after=$(node scripts/check-platform-conformance.mjs --contract | sed -n 's/^version=//p')
|
|
110
130
|
git add --all
|
|
111
131
|
conflicts=$(git grep --cached -l -E '^(<<<<<<<|>>>>>>>)( |$)' -- . || true)
|
|
112
132
|
{
|
|
113
133
|
echo "changed=true"
|
|
114
|
-
echo "
|
|
115
|
-
echo "tag=$after"
|
|
134
|
+
echo "tag=${after:-$(git rev-parse --short HEAD)}"
|
|
116
135
|
echo "files<<EOF"
|
|
117
136
|
git diff --cached --name-status
|
|
118
137
|
echo "EOF"
|
|
@@ -121,14 +140,19 @@ jobs:
|
|
|
121
140
|
echo "EOF"
|
|
122
141
|
} >> "$GITHUB_OUTPUT"
|
|
123
142
|
|
|
124
|
-
|
|
143
|
+
# One sync pull request at a time. An open one for the same version is left alone, so the
|
|
144
|
+
# conflict resolutions pushed to it survive; a newer version replaces its branch and its text.
|
|
145
|
+
- name: Open or update the sync pull request
|
|
125
146
|
if: steps.update.outputs.changed == 'true'
|
|
126
147
|
env:
|
|
127
148
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
128
149
|
REPO: ${{ github.repository }}
|
|
129
150
|
BASE: ${{ github.event.repository.default_branch }}
|
|
130
|
-
|
|
151
|
+
SOURCE_NAME: ${{ steps.contract.outputs.source_name }}
|
|
152
|
+
SOURCE_WEB: ${{ steps.contract.outputs.source_web }}
|
|
153
|
+
BEFORE: ${{ steps.contract.outputs.version }}
|
|
131
154
|
TAG: ${{ steps.update.outputs.tag }}
|
|
155
|
+
UPDATE: ${{ steps.contract.outputs.update }}
|
|
132
156
|
FILES: ${{ steps.update.outputs.files }}
|
|
133
157
|
CONFLICTS: ${{ steps.update.outputs.conflicts }}
|
|
134
158
|
GIT_CONFIG_COUNT: "1"
|
|
@@ -140,25 +164,29 @@ jobs:
|
|
|
140
164
|
GIT_COMMITTER_EMAIL: ${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com
|
|
141
165
|
run: |
|
|
142
166
|
set -euo pipefail
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
marker="<!-- platform-template-sync: $TAG -->"
|
|
168
|
+
open=$(gh pr list --repo "$REPO" --head "$SYNC_BRANCH" --state open --json number --jq '.[0].number // empty')
|
|
169
|
+
if [ -n "$open" ] && gh pr view "$open" --repo "$REPO" --json body --jq .body | grep -qF -- "$marker"; then
|
|
170
|
+
echo "Pull request #$open already syncs $TAG. Nothing to do."
|
|
146
171
|
exit 0
|
|
147
172
|
fi
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
git
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
title="chore(platform): sync $SOURCE_NAME to $TAG"
|
|
174
|
+
git switch --create "$SYNC_BRANCH"
|
|
175
|
+
git commit --quiet --message "$title"
|
|
176
|
+
git push --force "https://github.com/$REPO.git" "HEAD:refs/heads/$SYNC_BRANCH"
|
|
177
|
+
case "$SOURCE_WEB" in
|
|
178
|
+
https://github.com/*) target="[\`$TAG\`]($SOURCE_WEB/releases/tag/$TAG)" ;;
|
|
179
|
+
*) target="\`$TAG\`" ;;
|
|
180
|
+
esac
|
|
154
181
|
body="$RUNNER_TEMP/sync-body.md"
|
|
155
182
|
{
|
|
156
|
-
echo "
|
|
183
|
+
echo "$marker"
|
|
184
|
+
echo "Sync of the code owned by $SOURCE_NAME from \`${BEFORE:-no recorded version}\` to $target, made by \`$UPDATE\`."
|
|
157
185
|
echo
|
|
158
186
|
if [ -n "$CONFLICTS" ]; then
|
|
159
187
|
echo "### Conflicts"
|
|
160
188
|
echo
|
|
161
|
-
echo "These files carry inline conflict markers where
|
|
189
|
+
echo "These files carry inline conflict markers where this repository had edited owned code. CI fails on them until they are resolved."
|
|
162
190
|
echo
|
|
163
191
|
printf '%s\n' "$CONFLICTS" | awk '{ printf "- `%s`\n", $0 }'
|
|
164
192
|
echo
|
|
@@ -167,5 +195,8 @@ jobs:
|
|
|
167
195
|
echo
|
|
168
196
|
printf '%s\n' "$FILES" | awk -F '\t' '{ printf "- `%s` %s\n", $NF, $1 }'
|
|
169
197
|
} > "$body"
|
|
170
|
-
|
|
171
|
-
|
|
198
|
+
if [ -n "$open" ]; then
|
|
199
|
+
gh pr edit "$open" --repo "$REPO" --title "$title" --body-file "$body"
|
|
200
|
+
else
|
|
201
|
+
gh pr create --repo "$REPO" --base "$BASE" --head "$SYNC_BRANCH" --title "$title" --body-file "$body"
|
|
202
|
+
fi
|