@logickernel/agileflow 0.17.1 → 0.21.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 +4 -3
- package/docs/guides/github-actions.md +26 -22
- package/package.json +1 -1
- package/src/github-push.js +10 -7
- package/src/utils.js +109 -7
package/README.md
CHANGED
|
@@ -61,6 +61,9 @@ on:
|
|
|
61
61
|
push:
|
|
62
62
|
branches: [main]
|
|
63
63
|
|
|
64
|
+
permissions:
|
|
65
|
+
contents: write
|
|
66
|
+
|
|
64
67
|
jobs:
|
|
65
68
|
version:
|
|
66
69
|
runs-on: ubuntu-latest
|
|
@@ -74,12 +77,10 @@ jobs:
|
|
|
74
77
|
node-version: '20'
|
|
75
78
|
|
|
76
79
|
- name: Create version tag
|
|
77
|
-
env:
|
|
78
|
-
AGILEFLOW_TOKEN: ${{ secrets.AGILEFLOW_TOKEN }}
|
|
79
80
|
run: npx @logickernel/agileflow github
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
AgileFlow automatically uses the built-in `GITHUB_TOKEN` — no secrets or custom tokens needed. Just grant `contents: write` permission in the workflow. You can also use a [Personal Access Token](./docs/guides/github-actions.md) if your organization restricts `GITHUB_TOKEN` permissions.
|
|
83
84
|
|
|
84
85
|
### GitLab CI
|
|
85
86
|
|
|
@@ -4,23 +4,7 @@ Two workflows: one that runs AgileFlow on push to main and creates a version tag
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## Step 1: Create
|
|
8
|
-
|
|
9
|
-
1. Go to **Settings → Developer settings → Personal access tokens → Fine-grained tokens**
|
|
10
|
-
2. Click **Generate new token**
|
|
11
|
-
3. Set:
|
|
12
|
-
- **Name**: `AgileFlow`
|
|
13
|
-
- **Repository access**: your repository
|
|
14
|
-
- **Permissions**: `Contents: Read and write`
|
|
15
|
-
4. Copy the token
|
|
16
|
-
|
|
17
|
-
## Step 2: Add the token as a secret
|
|
18
|
-
|
|
19
|
-
1. In your repository: **Settings → Secrets and variables → Actions**
|
|
20
|
-
2. Click **New repository secret**
|
|
21
|
-
3. Name: `AGILEFLOW_TOKEN`, value: your token
|
|
22
|
-
|
|
23
|
-
## Step 3: Create the versioning workflow
|
|
7
|
+
## Step 1: Create the versioning workflow
|
|
24
8
|
|
|
25
9
|
`.github/workflows/version.yml`:
|
|
26
10
|
|
|
@@ -30,6 +14,9 @@ on:
|
|
|
30
14
|
push:
|
|
31
15
|
branches: [main]
|
|
32
16
|
|
|
17
|
+
permissions:
|
|
18
|
+
contents: write
|
|
19
|
+
|
|
33
20
|
jobs:
|
|
34
21
|
version:
|
|
35
22
|
runs-on: ubuntu-latest
|
|
@@ -43,14 +30,31 @@ jobs:
|
|
|
43
30
|
node-version: '20'
|
|
44
31
|
|
|
45
32
|
- name: Create version tag
|
|
46
|
-
env:
|
|
47
|
-
AGILEFLOW_TOKEN: ${{ secrets.AGILEFLOW_TOKEN }}
|
|
48
33
|
run: npx @logickernel/agileflow github
|
|
49
34
|
```
|
|
50
35
|
|
|
36
|
+
That's it. AgileFlow automatically picks up the `GITHUB_TOKEN` that GitHub Actions provides. The `permissions: contents: write` block grants it permission to create tags.
|
|
37
|
+
|
|
51
38
|
`fetch-depth: 0` is required — without it, AgileFlow can only see a shallow clone and cannot find the last version tag.
|
|
52
39
|
|
|
53
|
-
|
|
40
|
+
### Using a Personal Access Token instead
|
|
41
|
+
|
|
42
|
+
If your organization restricts `GITHUB_TOKEN` permissions or you need cross-repository tagging, use a PAT:
|
|
43
|
+
|
|
44
|
+
1. Go to **Settings → Developer settings → Personal access tokens → Fine-grained tokens**
|
|
45
|
+
2. Click **Generate new token**
|
|
46
|
+
3. Set:
|
|
47
|
+
- **Name**: `AgileFlow`
|
|
48
|
+
- **Repository access**: your repository
|
|
49
|
+
- **Permissions**: `Contents: Read and write`
|
|
50
|
+
4. Copy the token
|
|
51
|
+
5. In your repository: **Settings → Secrets and variables → Actions**
|
|
52
|
+
6. Click **New repository secret**
|
|
53
|
+
7. Name: `AGILEFLOW_TOKEN`, value: your token
|
|
54
|
+
|
|
55
|
+
AgileFlow checks `AGILEFLOW_TOKEN` first, then falls back to `GITHUB_TOKEN`. When `AGILEFLOW_TOKEN` is set, no `permissions` block is needed.
|
|
56
|
+
|
|
57
|
+
## Step 3: Create the release workflow
|
|
54
58
|
|
|
55
59
|
`.github/workflows/release.yml`:
|
|
56
60
|
|
|
@@ -99,9 +103,9 @@ If no bump is needed (all commits are `chore`, `docs`, etc.), AgileFlow exits wi
|
|
|
99
103
|
|
|
100
104
|
## Troubleshooting
|
|
101
105
|
|
|
102
|
-
**"
|
|
106
|
+
**"No authentication token found"** — Neither `AGILEFLOW_TOKEN` nor `GITHUB_TOKEN` is available. If running in GitHub Actions, ensure the workflow has `permissions: contents: write`. If using a PAT, verify the `AGILEFLOW_TOKEN` secret exists.
|
|
103
107
|
|
|
104
|
-
**"Resource not accessible by integration" / 403** — The token lacks `
|
|
108
|
+
**"Resource not accessible by integration" / 403** — The token lacks `contents: write` permission. Add `permissions: contents: write` to your workflow, or regenerate your PAT with the correct scope.
|
|
105
109
|
|
|
106
110
|
**"Bad credentials" / 401** — The token has expired or was revoked. Regenerate and update the secret.
|
|
107
111
|
|
package/package.json
CHANGED
package/src/github-push.js
CHANGED
|
@@ -131,17 +131,20 @@ function makeRequest({ method, path, accessToken, body }) {
|
|
|
131
131
|
* @returns {Promise<void>}
|
|
132
132
|
*/
|
|
133
133
|
async function pushTag(tagName, message, remote = 'origin') {
|
|
134
|
-
const accessToken = process.env.AGILEFLOW_TOKEN;
|
|
134
|
+
const accessToken = process.env.AGILEFLOW_TOKEN || process.env.GITHUB_TOKEN;
|
|
135
135
|
const repository = process.env.GITHUB_REPOSITORY;
|
|
136
136
|
const commitSha = process.env.GITHUB_SHA;
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
if (!accessToken) {
|
|
139
139
|
throw new Error(
|
|
140
|
-
`
|
|
141
|
-
`
|
|
142
|
-
`
|
|
143
|
-
`
|
|
144
|
-
`
|
|
140
|
+
`No authentication token found.\n\n` +
|
|
141
|
+
`AgileFlow looks for AGILEFLOW_TOKEN or GITHUB_TOKEN (in that order).\n\n` +
|
|
142
|
+
`In GitHub Actions, GITHUB_TOKEN is available automatically — just add\n` +
|
|
143
|
+
`permissions to your workflow:\n` +
|
|
144
|
+
` permissions:\n` +
|
|
145
|
+
` contents: write\n\n` +
|
|
146
|
+
`Or use a Personal Access Token with "contents: write" permission\n` +
|
|
147
|
+
`and store it as a secret named AGILEFLOW_TOKEN.`
|
|
145
148
|
);
|
|
146
149
|
}
|
|
147
150
|
|
package/src/utils.js
CHANGED
|
@@ -224,9 +224,57 @@ function parseConventionalCommit(message) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
*
|
|
228
|
-
* @param {Array} commits -
|
|
229
|
-
* @returns {{latestVersion: string|null,
|
|
227
|
+
* Finds the highest semver version tag in a list of commits.
|
|
228
|
+
* @param {Array} commits - The commits to scan
|
|
229
|
+
* @returns {{latestVersion: string|null, taggedCommitHash: string|null}}
|
|
230
|
+
*/
|
|
231
|
+
function findLatestVersionTag(commits) {
|
|
232
|
+
if (!commits?.length) {
|
|
233
|
+
return { latestVersion: null, taggedCommitHash: null };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let bestHash = null;
|
|
237
|
+
let bestVersion = null;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < commits.length; i++) {
|
|
240
|
+
const semverTags = commits[i].tags?.filter(tag => SEMVER_PATTERN.test(tag));
|
|
241
|
+
if (!semverTags?.length) continue;
|
|
242
|
+
|
|
243
|
+
const highest = semverTags.sort((a, b) => {
|
|
244
|
+
const pa = parseVersion(a);
|
|
245
|
+
const pb = parseVersion(b);
|
|
246
|
+
if (pb.major !== pa.major) return pb.major - pa.major;
|
|
247
|
+
if (pb.minor !== pa.minor) return pb.minor - pa.minor;
|
|
248
|
+
return pb.patch - pa.patch;
|
|
249
|
+
})[0];
|
|
250
|
+
|
|
251
|
+
if (!bestVersion) {
|
|
252
|
+
bestVersion = highest;
|
|
253
|
+
bestHash = commits[i].hash;
|
|
254
|
+
} else {
|
|
255
|
+
const pBest = parseVersion(bestVersion);
|
|
256
|
+
const pCand = parseVersion(highest);
|
|
257
|
+
const isHigher =
|
|
258
|
+
pCand.major > pBest.major ||
|
|
259
|
+
(pCand.major === pBest.major && pCand.minor > pBest.minor) ||
|
|
260
|
+
(pCand.major === pBest.major && pCand.minor === pBest.minor && pCand.patch > pBest.patch);
|
|
261
|
+
if (isHigher) {
|
|
262
|
+
bestVersion = highest;
|
|
263
|
+
bestHash = commits[i].hash;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { latestVersion: bestVersion, taggedCommitHash: bestHash };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Expands commit info by finding the latest version tag and returning
|
|
273
|
+
* only commits since that tag. Uses positional slicing from a date-sorted
|
|
274
|
+
* commit list — for accurate results with merge commits, prefer using
|
|
275
|
+
* findLatestVersionTag + getCommitsSinceTag (as processVersionInfo does).
|
|
276
|
+
* @param {Array} commits - All commits (date-sorted, newest first)
|
|
277
|
+
* @returns {{latestVersion: string|null, commits: Array}}
|
|
230
278
|
*/
|
|
231
279
|
function expandCommitInfo(commits) {
|
|
232
280
|
if (!commits?.length) {
|
|
@@ -548,6 +596,45 @@ function getAllBranchCommits(branch) {
|
|
|
548
596
|
}
|
|
549
597
|
}
|
|
550
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Retrieves commits reachable from the branch but not from the given tag,
|
|
601
|
+
* using git's native range selection so that date ordering cannot cause
|
|
602
|
+
* commits from merged branches to be missed.
|
|
603
|
+
* @param {string} tagHash - The commit hash of the version tag
|
|
604
|
+
* @param {string} branchSha - The resolved SHA of the branch tip
|
|
605
|
+
* @returns {Array<{hash: string, datetime: string, author: string, message: string, tags: Array<string>}>}
|
|
606
|
+
*/
|
|
607
|
+
function getCommitsSinceTag(tagHash, branchSha) {
|
|
608
|
+
const tagMap = buildTagMap();
|
|
609
|
+
const RS = '\x1E';
|
|
610
|
+
const COMMIT_SEP = `${RS}${RS}`;
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const logCmd = `git log --format=%H${RS}%ai${RS}%an${RS}%B${COMMIT_SEP} ${tagHash}..${branchSha}`;
|
|
614
|
+
const output = runWithOutput(logCmd).trim();
|
|
615
|
+
if (!output) return [];
|
|
616
|
+
|
|
617
|
+
return output
|
|
618
|
+
.split(COMMIT_SEP)
|
|
619
|
+
.filter(block => block.trim())
|
|
620
|
+
.map(block => {
|
|
621
|
+
const parts = block.split(RS);
|
|
622
|
+
if (parts.length < 4) return null;
|
|
623
|
+
const hash = parts[0].trim();
|
|
624
|
+
return {
|
|
625
|
+
hash,
|
|
626
|
+
datetime: parts[1].trim(),
|
|
627
|
+
author: parts[2].trim(),
|
|
628
|
+
message: parts.slice(3).join(RS).trim(),
|
|
629
|
+
tags: tagMap.get(hash) || [],
|
|
630
|
+
};
|
|
631
|
+
})
|
|
632
|
+
.filter(Boolean);
|
|
633
|
+
} catch {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
551
638
|
/**
|
|
552
639
|
* Processes version information for the current branch.
|
|
553
640
|
* @returns {Promise<{currentVersion: string|null, newVersion: string|null, commits: Array, changelog: string}>}
|
|
@@ -556,12 +643,25 @@ async function processVersionInfo() {
|
|
|
556
643
|
ensureGitRepo();
|
|
557
644
|
const branch = getCurrentBranch();
|
|
558
645
|
fetchTags();
|
|
559
|
-
|
|
646
|
+
|
|
560
647
|
const allCommits = getAllBranchCommits(branch);
|
|
561
|
-
const
|
|
562
|
-
|
|
648
|
+
const { latestVersion, taggedCommitHash } = findLatestVersionTag(allCommits);
|
|
649
|
+
|
|
650
|
+
let commits;
|
|
651
|
+
if (taggedCommitHash) {
|
|
652
|
+
// Use git's range selection (tag..HEAD) to correctly identify all commits
|
|
653
|
+
// since the tag, regardless of commit date ordering. This fixes an issue
|
|
654
|
+
// where commits from merged feature branches could be missed because their
|
|
655
|
+
// dates are older than the tagged commit.
|
|
656
|
+
const branchSha = allCommits[0]?.hash;
|
|
657
|
+
commits = branchSha ? getCommitsSinceTag(taggedCommitHash, branchSha) : [];
|
|
658
|
+
} else {
|
|
659
|
+
commits = allCommits;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const expandedInfo = { latestVersion, commits };
|
|
563
663
|
const { newVersion, changelog } = calculateNextVersionAndChangelog(expandedInfo);
|
|
564
|
-
|
|
664
|
+
|
|
565
665
|
return {
|
|
566
666
|
currentVersion: latestVersion,
|
|
567
667
|
newVersion,
|
|
@@ -577,6 +677,8 @@ module.exports = {
|
|
|
577
677
|
getCurrentBranch,
|
|
578
678
|
fetchTags,
|
|
579
679
|
getAllBranchCommits,
|
|
680
|
+
findLatestVersionTag,
|
|
681
|
+
getCommitsSinceTag,
|
|
580
682
|
expandCommitInfo,
|
|
581
683
|
calculateNextVersionAndChangelog,
|
|
582
684
|
processVersionInfo,
|