@logickernel/agileflow 0.1.0 → 0.2.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.
@@ -5,205 +5,189 @@ Welcome to AgileFlow! This guide will help you get up and running with AgileFlow
5
5
  ## What You'll Learn
6
6
 
7
7
  By the end of this guide, you'll have:
8
- - AgileFlow installed in your GitLab project
9
- - A working CI/CD pipeline that automatically generates versions
10
- - Understanding of how the version-centric approach works
11
- - ✅ Confidence to customize the pipeline for your needs
8
+ - AgileFlow running in your project
9
+ - Automatic semantic versioning based on conventional commits
10
+ - Understanding of how the version-centric approach works
12
11
 
13
12
  ## Prerequisites
14
13
 
15
14
  Before you begin, ensure you have:
16
- - A GitLab project with CI/CD enabled
17
- - Access to modify `.gitlab-ci.yml` files and CI/CD variables
18
- - Basic understanding of Git and CI/CD concepts
15
+ - Node.js 14+ installed (for local usage)
16
+ - A Git repository with commit history
17
+ - Basic understanding of Git and conventional commits
19
18
 
20
- ## Quick Start (5 minutes)
19
+ ## Quick Start
21
20
 
22
- ### Step 1: Include the AgileFlow Template
21
+ ### Local Usage
23
22
 
24
- Add this line to the top of your `.gitlab-ci.yml` file:
23
+ Run AgileFlow directly with npx to see your current and next version:
25
24
 
26
- ```yaml
27
- include:
28
- - remote: https://code.logickernel.com/kernel/agileflow/-/raw/main/templates/AgileFlow.gitlab-ci.yml
25
+ ```bash
26
+ npx @logickernel/agileflow
29
27
  ```
30
28
 
31
- ### Step 2: Configure the AGILEFLOW_TOKEN
32
-
33
- AgileFlow needs a GitLab access token to create version tags via the API. Set this in your GitLab project:
34
-
35
- 1. Go to **Settings > CI/CD > Variables**
36
- 2. Add the `AGILEFLOW_TOKEN` variable:
37
-
38
- | Variable | Value | Type | Protect | Mask |
39
- |----------|-------|------|---------|------|
40
- | `AGILEFLOW_TOKEN` | Your GitLab API token | Variable | Yes | No |
41
-
42
- **Creating the AGILEFLOW_TOKEN:**
43
- - **Project Access Token (Recommended)**: Go to **Settings > Access Tokens** in your project
44
- - **Personal Access Token**: Go to your user **Settings > Access Tokens**
45
- - Set **Name**: `AgileFlow Bot`, **Scopes**: `api`, **Role**: `maintainer` or higher
46
-
47
- ### Step 3: Add Your First Job
48
-
49
- Below the include statement, add a simple build job:
50
-
51
- ```yaml
52
- build:
53
- stage: build
54
- script:
55
- - echo "Building version ${VERSION}"
56
- - docker build -t myapp:${VERSION} .
57
- - docker push myapp:${VERSION}
58
- needs:
59
- - agileflow
29
+ Example output:
60
30
  ```
31
+ Current version: v1.2.3
32
+ Next version: v1.2.4
33
+ Commits since current version: 3
61
34
 
62
- ### Step 4: Commit and Push
35
+ Changelog:
36
+ ### fix
37
+ - resolve authentication issue
38
+ - correct null handling in user lookup
63
39
 
64
- ```bash
65
- git add .gitlab-ci.yml
66
- git commit -m "feat: add AgileFlow CI/CD pipeline"
67
- git push
40
+ ### docs
41
+ - update README with usage examples
68
42
  ```
69
43
 
70
- That's it! 🎉 Your first AgileFlow pipeline is now running.
44
+ ### Quiet Mode
71
45
 
72
- ## What Happens Next
46
+ Use `--quiet` to only output the next version (useful for scripts):
73
47
 
74
- 1. **Pipeline Starts**: GitLab CI automatically detects your changes
75
- 2. **Version Generation**: AgileFlow analyzes your commit history and generates the next semantic version
76
- 3. **Build Process**: Your build job runs with the generated version
77
- 4. **Version Tag**: A new version tag is created via GitLab API (not git push)
78
-
79
- ## Understanding the Pipeline
48
+ ```bash
49
+ VERSION=$(npx @logickernel/agileflow --quiet)
50
+ echo "Next version will be: $VERSION"
51
+ ```
80
52
 
81
- Your pipeline now has 6 stages:
53
+ ## CI/CD Integration
54
+
55
+ ### GitLab CI
56
+
57
+ 1. **Configure the AGILEFLOW_TOKEN**
58
+
59
+ Go to **Settings > CI/CD > Variables** and add:
60
+
61
+ | Variable | Value | Protect | Mask |
62
+ |----------|-------|---------|------|
63
+ | `AGILEFLOW_TOKEN` | Your GitLab API token | Yes | Yes |
64
+
65
+ Create the token at **Settings > Access Tokens** with:
66
+ - **Name**: AgileFlow Bot
67
+ - **Role**: Maintainer
68
+ - **Scopes**: api
69
+
70
+ 2. **Add AgileFlow to your pipeline**
71
+
72
+ ```yaml
73
+ stages:
74
+ - version
75
+ - build
76
+
77
+ agileflow:
78
+ stage: version
79
+ image: node:20-alpine
80
+ script:
81
+ - npx @logickernel/agileflow gitlab
82
+ only:
83
+ - main
84
+
85
+ build:
86
+ stage: build
87
+ script:
88
+ - echo "Building..."
89
+ needs:
90
+ - agileflow
91
+ ```
92
+
93
+ ### GitHub Actions
94
+
95
+ 1. **Configure the AGILEFLOW_TOKEN**
96
+
97
+ Go to **Settings > Secrets and variables > Actions** and add a secret:
98
+ - **Name**: `AGILEFLOW_TOKEN`
99
+ - **Value**: A Personal Access Token with `contents: write` permission
100
+
101
+ 2. **Add AgileFlow to your workflow**
102
+
103
+ ```yaml
104
+ name: Release
105
+ on:
106
+ push:
107
+ branches: [main]
108
+
109
+ jobs:
110
+ version:
111
+ runs-on: ubuntu-latest
112
+ steps:
113
+ - uses: actions/checkout@v4
114
+ with:
115
+ fetch-depth: 0 # Required for version history
116
+
117
+ - uses: actions/setup-node@v4
118
+ with:
119
+ node-version: '20'
120
+
121
+ - name: Create version tag
122
+ env:
123
+ AGILEFLOW_TOKEN: ${{ secrets.AGILEFLOW_TOKEN }}
124
+ run: npx @logickernel/agileflow github
125
+ ```
126
+
127
+ ### Native Git Push
128
+
129
+ If you prefer using native git commands (requires git credentials):
82
130
 
83
- ```
84
- version test → build → deploy → e2e → clean
131
+ ```bash
132
+ npx @logickernel/agileflow push
85
133
  ```
86
134
 
87
- - **version**: AgileFlow generates semantic versions automatically using GitLab API
88
- - **test**: Run tests against source code before building (add your test jobs here)
89
- - **build**: Your application builds with the generated version
90
- - **deploy**: Deploy the versioned artifacts (add your deployment jobs here)
91
- - **e2e**: Run end-to-end tests against deployed versions (add your e2e test jobs here)
92
- - **clean**: Cleanup temporary resources (optional)
135
+ This creates an annotated tag and pushes it to the origin remote.
93
136
 
94
137
  ## How AgileFlow Works
95
138
 
96
- ### No Git Push Required
97
- - **AgileFlow uses GitLab API** to create version tags
98
- - **No repository write permissions** needed for CI/CD jobs
99
- - **More secure** than traditional git push approaches
100
- - **Works with protected branches** without special permissions
101
-
102
- ### Version Generation
103
- - **Analyzes commit messages** since the last version
104
- - **Uses conventional commits** to determine version bump type
105
- - **Creates semantic versions** automatically (v1.0.0, v1.0.1, etc.)
106
- - **Generates release notes** from commit history
139
+ ### Version Calculation
140
+ - Analyzes commit messages since the last version tag
141
+ - Uses conventional commits to determine version bump type
142
+ - Generates semantic versions automatically (v1.0.0, v1.0.1, etc.)
107
143
 
108
- ## Next Steps
144
+ ### Version Bump Rules
109
145
 
110
- Now that you have the basics working, explore these areas:
111
-
112
- ### 1. Add Deployment Jobs
113
- ```yaml
114
- deploy-staging:
115
- stage: deploy
116
- script:
117
- - kubectl set image deployment/myapp myapp=myapp:${VERSION}
118
- environment:
119
- name: staging
120
- needs:
121
- - build
122
- ```
146
+ | Commit Type | Example | Version Bump |
147
+ |-------------|---------|--------------|
148
+ | Breaking change | `feat!: redesign API` | Major (1.0.0 2.0.0) |
149
+ | Feature | `feat: add login` | Minor (1.0.0 → 1.1.0) |
150
+ | Fix | `fix: resolve crash` | Patch (1.0.0 → 1.0.1) |
151
+ | Performance | `perf: optimize query` | Patch |
152
+ | Refactor | `refactor: simplify logic` | Patch |
153
+ | Docs only | `docs: update README` | No bump |
154
+ | Chore | `chore: update deps` | No bump |
123
155
 
124
- ### 2. Add Testing
125
- ```yaml
126
- # Unit tests run before building
127
- unit-tests:
128
- stage: test
129
- script:
130
- - npm test
131
- - npm run lint
132
- needs:
133
- - agileflow
134
-
135
- # Integration tests run after deployment
136
- integration-tests:
137
- stage: e2e
138
- script:
139
- - ./run-tests.sh --version ${VERSION}
140
- needs:
141
- - deploy-staging
142
- ```
156
+ ### No Bump Needed
143
157
 
144
- ### 3. Customize for Multiple Services
145
- ```yaml
146
- build-backend:
147
- stage: build
148
- script:
149
- - docker build -t backend:${VERSION} ./backend
150
- - docker push backend:${VERSION}
151
- needs:
152
- - agileflow
153
-
154
- build-frontend:
155
- stage: build
156
- script:
157
- - docker build -t frontend:${VERSION} ./frontend
158
- - docker push frontend:${VERSION}
159
- needs:
160
- - agileflow
161
- ```
158
+ If all commits since the last version are docs/chore/style types, AgileFlow will report "no bump needed" and skip tag creation in push commands.
162
159
 
163
160
  ## Common Questions
164
161
 
165
162
  ### Q: How does AgileFlow determine the next version?
166
- A: AgileFlow analyzes your commit messages using conventional commits. Each merge to main increments the patch version (v1.0.0 v1.0.1). Use `feat:` for minor versions and `feat!:` for major versions.
167
-
168
- ### Q: Can I use this with existing CI/CD pipelines?
169
- A: Yes! AgileFlow is designed to work alongside existing pipelines. Just include the template and gradually migrate your jobs to use the `${VERSION}` variable.
163
+ A: AgileFlow analyzes your commit messages using conventional commits. Features bump the minor version, fixes bump the patch version, and breaking changes bump the major version.
170
164
 
171
- ### Q: What if I need different versions for different environments?
172
- A: AgileFlow generates one version per pipeline run. Deploy the same version everywhere to eliminate environment drift. Use environment-specific configuration instead of different versions.
165
+ ### Q: What if there's no version tag yet?
166
+ A: AgileFlow starts from v0.0.0 and calculates the first version based on your commits.
173
167
 
174
- ### Q: How do I rollback to a previous version?
175
- A: Simply redeploy the previous version tag. Since all environments use the same version, rollbacks are consistent and predictable.
168
+ ### Q: Can I use this locally before pushing?
169
+ A: Yes! Run `npx @logickernel/agileflow` to preview the next version without creating any tags.
176
170
 
177
- ### Q: Why doesn't AgileFlow need git push permissions?
178
- A: AgileFlow uses the GitLab API to create tags remotely instead of pushing them with git commands. This is more secure and doesn't require repository write access.
171
+ ### Q: What happens if no version bump is needed?
172
+ A: The push commands (`push`, `gitlab`, `github`) will skip tag creation and exit successfully.
179
173
 
180
174
  ## Troubleshooting
181
175
 
182
- ### Pipeline Fails on Version Stage
183
- - Check that the `AGILEFLOW_TOKEN` is set correctly
184
- - Ensure the token has `api` scope and `maintainer` role
185
- - Verify your GitLab CI/CD settings
186
- - Check the `agileflow` job logs for specific errors
187
-
188
- ### VERSION Variable Not Available
189
- - Ensure the `agileflow` job completed successfully
190
- - Check that your jobs have `needs: - agileflow` dependency
191
- - Verify the dotenv artifact is properly configured
176
+ ### "Not a git repository" Error
177
+ - Ensure you're running AgileFlow from within a git repository
178
+ - Check that the `.git` directory exists
192
179
 
193
- ### Build Jobs Fail
194
- - Check that you're using `${VERSION}` correctly
195
- - Ensure proper job dependencies with `needs:`
196
- - Verify your build scripts work with the version variable
180
+ ### "AGILEFLOW_TOKEN not set" Error
181
+ - Ensure the environment variable is configured in your CI/CD settings
182
+ - Verify the token has the required permissions
197
183
 
198
- ## Getting Help
184
+ ### No Version Bump Detected
185
+ - Ensure you're using conventional commit format
186
+ - Check that there are commits since the last version tag
187
+ - Verify commits include bump-triggering types (feat, fix, perf, etc.)
199
188
 
200
- - 📚 [Complete Documentation](./README.md) - Browse all available guides
201
- - 🔧 [GitLab CI Template Reference](./gitlab-ci-template.md) - Detailed template documentation
202
- - 💡 [Version-Centric CI/CD Approach](./version-centric-cicd.md) - Deep dive into the methodology
203
- - 🐛 [Troubleshooting](./troubleshooting.md) - Common issues and solutions
204
-
205
- ## Congratulations! 🎉
206
-
207
- You've successfully set up AgileFlow and are now using a modern, version-centric CI/CD approach. Your deployments will be more predictable, your rollbacks will be simpler, and your team will have better visibility into what's running where.
189
+ ## Next Steps
208
190
 
209
- Ready to dive deeper? Explore the [advanced topics](./README.md#advanced-topics) or customize your pipeline further!
191
+ - Read the [CLI Reference](./cli-reference.md) for all available commands
192
+ - Learn about [Conventional Commits](./conventional-commits.md)
193
+ - Explore [Version-Centric CI/CD](./version-centric-cicd.md) methodology
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logickernel/agileflow",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Automatic semantic versioning and changelog generation based on conventional commits",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const { run } = require('./utils');
4
+
5
+ /**
6
+ * Creates an annotated tag and pushes it to the remote repository.
7
+ * Uses native git commands - requires git credentials to be configured.
8
+ * @param {string} tagName - The tag name (e.g., "v1.2.3")
9
+ * @param {string} message - The tag message (changelog)
10
+ * @returns {Promise<void>}
11
+ */
12
+ async function pushTag(tagName, message) {
13
+ const safeTag = String(tagName).replace(/"/g, '\\"');
14
+ const safeMsg = String(message).replace(/"/g, '\\"');
15
+
16
+ // Create annotated tag
17
+ run(`git tag -a "${safeTag}" -m "${safeMsg}"`);
18
+
19
+ // Push to origin
20
+ run(`git push origin "${safeTag}"`);
21
+ }
22
+
23
+ module.exports = {
24
+ pushTag,
25
+ };
26
+
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+
5
+ /**
6
+ * Creates a tag via the GitHub API.
7
+ * GitHub requires creating a tag object first, then a reference.
8
+ * @param {string} tagName - The tag name
9
+ * @param {string} message - The tag message
10
+ * @param {string} repository - GitHub repository (e.g., "owner/repo")
11
+ * @param {string} accessToken - GitHub access token
12
+ * @param {string} commitSha - The commit SHA to tag
13
+ * @returns {Promise<Object>} API response
14
+ */
15
+ async function createTagViaAPI(tagName, message, repository, accessToken, commitSha) {
16
+ // Step 1: Create an annotated tag object
17
+ const tagObject = await makeRequest({
18
+ method: 'POST',
19
+ path: `/repos/${repository}/git/tags`,
20
+ accessToken,
21
+ body: {
22
+ tag: tagName,
23
+ message: message,
24
+ object: commitSha,
25
+ type: 'commit',
26
+ },
27
+ });
28
+
29
+ // Step 2: Create a reference pointing to the tag object
30
+ await makeRequest({
31
+ method: 'POST',
32
+ path: `/repos/${repository}/git/refs`,
33
+ accessToken,
34
+ body: {
35
+ ref: `refs/tags/${tagName}`,
36
+ sha: tagObject.sha,
37
+ },
38
+ });
39
+
40
+ return tagObject;
41
+ }
42
+
43
+ /**
44
+ * Makes an HTTPS request to the GitHub API.
45
+ * @param {{method: string, path: string, accessToken: string, body?: Object}} options
46
+ * @returns {Promise<Object>}
47
+ */
48
+ function makeRequest({ method, path, accessToken, body }) {
49
+ return new Promise((resolve, reject) => {
50
+ const postData = body ? JSON.stringify(body) : '';
51
+
52
+ const options = {
53
+ hostname: 'api.github.com',
54
+ port: 443,
55
+ path: path,
56
+ method: method,
57
+ headers: {
58
+ 'Authorization': `Bearer ${accessToken}`,
59
+ 'Accept': 'application/vnd.github+json',
60
+ 'X-GitHub-Api-Version': '2022-11-28',
61
+ 'User-Agent': 'AgileFlow',
62
+ 'Content-Type': 'application/json',
63
+ ...(body && { 'Content-Length': Buffer.byteLength(postData) }),
64
+ },
65
+ };
66
+
67
+ const req = https.request(options, (res) => {
68
+ let data = '';
69
+
70
+ res.on('data', (chunk) => {
71
+ data += chunk;
72
+ });
73
+
74
+ res.on('end', () => {
75
+ if (res.statusCode >= 200 && res.statusCode < 300) {
76
+ resolve(data ? JSON.parse(data) : {});
77
+ } else {
78
+ let errorMessage = `GitHub API request failed with status ${res.statusCode}`;
79
+ try {
80
+ const errorData = JSON.parse(data);
81
+ if (errorData.message) {
82
+ errorMessage += `: ${errorData.message}`;
83
+ }
84
+ if (res.statusCode === 401) {
85
+ errorMessage += '\n\nAuthentication failed. The AGILEFLOW_TOKEN is invalid or expired.';
86
+ errorMessage += '\nTo fix this:';
87
+ errorMessage += '\n1. Go to your repository Settings > Secrets and variables > Actions';
88
+ errorMessage += '\n2. Create a secret named AGILEFLOW_TOKEN with a Personal Access Token';
89
+ errorMessage += '\n3. The token needs "contents: write" permission';
90
+ } else if (res.statusCode === 403) {
91
+ errorMessage += '\n\nPermission denied. The AGILEFLOW_TOKEN needs "contents: write" permission.';
92
+ errorMessage += '\nTo fix this:';
93
+ errorMessage += '\n1. Ensure your token has "contents: write" scope';
94
+ errorMessage += '\n2. If using GITHUB_TOKEN, add permissions to your workflow';
95
+ } else if (res.statusCode === 422) {
96
+ errorMessage += '\n\nThe tag may already exist or the reference is invalid.';
97
+ }
98
+ } catch {
99
+ if (data) {
100
+ errorMessage += `\nResponse: ${data}`;
101
+ }
102
+ }
103
+ reject(new Error(errorMessage));
104
+ }
105
+ });
106
+ });
107
+
108
+ req.on('error', (error) => {
109
+ reject(new Error(`Network error: ${error.message}`));
110
+ });
111
+
112
+ if (postData) {
113
+ req.write(postData);
114
+ }
115
+ req.end();
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Pushes a tag to GitHub via the API.
121
+ * Requires AGILEFLOW_TOKEN environment variable.
122
+ * Uses GITHUB_REPOSITORY and GITHUB_SHA from GitHub Actions environment.
123
+ * @param {string} tagName - The tag name
124
+ * @param {string} message - The tag message
125
+ * @returns {Promise<void>}
126
+ */
127
+ async function pushTag(tagName, message) {
128
+ const accessToken = process.env.AGILEFLOW_TOKEN;
129
+ const repository = process.env.GITHUB_REPOSITORY;
130
+ const commitSha = process.env.GITHUB_SHA;
131
+
132
+ if (!accessToken) {
133
+ throw new Error(
134
+ `AGILEFLOW_TOKEN environment variable is required but not set.\n\n` +
135
+ `To fix this:\n` +
136
+ `1. Create a Personal Access Token with "contents: write" permission\n` +
137
+ `2. Add it as a repository secret named AGILEFLOW_TOKEN\n` +
138
+ `3. In your workflow, add: env: AGILEFLOW_TOKEN: \${{ secrets.AGILEFLOW_TOKEN }}`
139
+ );
140
+ }
141
+
142
+ if (!repository) {
143
+ throw new Error('GITHUB_REPOSITORY environment variable is not set. Are you running inside GitHub Actions?');
144
+ }
145
+
146
+ if (!commitSha) {
147
+ throw new Error('GITHUB_SHA environment variable is not set. Are you running inside GitHub Actions?');
148
+ }
149
+
150
+ await createTagViaAPI(tagName, message || tagName, repository, accessToken, commitSha);
151
+ }
152
+
153
+ module.exports = {
154
+ pushTag,
155
+ };
156
+
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+
5
+ /**
6
+ * Creates a tag via the GitLab API.
7
+ * @param {string} tagName - The tag name
8
+ * @param {string} message - The tag message
9
+ * @param {string} projectPath - GitLab project path (e.g., "group/project")
10
+ * @param {string} serverHost - GitLab server hostname
11
+ * @param {string} accessToken - GitLab access token
12
+ * @param {string} ref - Git ref to tag (branch name or commit SHA)
13
+ * @returns {Promise<Object>} API response
14
+ */
15
+ function createTagViaAPI(tagName, message, projectPath, serverHost, accessToken, ref) {
16
+ return new Promise((resolve, reject) => {
17
+ const projectId = encodeURIComponent(projectPath);
18
+
19
+ const postData = JSON.stringify({
20
+ tag_name: tagName,
21
+ ref: ref,
22
+ message: message,
23
+ });
24
+
25
+ const options = {
26
+ hostname: serverHost,
27
+ port: 443,
28
+ path: `/api/v4/projects/${projectId}/repository/tags`,
29
+ method: 'POST',
30
+ headers: {
31
+ 'PRIVATE-TOKEN': accessToken,
32
+ 'Content-Type': 'application/json',
33
+ 'Content-Length': Buffer.byteLength(postData),
34
+ },
35
+ };
36
+
37
+ const req = https.request(options, (res) => {
38
+ let data = '';
39
+
40
+ res.on('data', (chunk) => {
41
+ data += chunk;
42
+ });
43
+
44
+ res.on('end', () => {
45
+ if (res.statusCode >= 200 && res.statusCode < 300) {
46
+ resolve(JSON.parse(data));
47
+ } else {
48
+ let errorMessage = `GitLab API request failed with status ${res.statusCode}`;
49
+ try {
50
+ const errorData = JSON.parse(data);
51
+ if (errorData.message) {
52
+ errorMessage += `: ${JSON.stringify(errorData.message)}`;
53
+ }
54
+ if (res.statusCode === 401) {
55
+ errorMessage += '\n\nAuthentication failed. The AGILEFLOW_TOKEN is invalid or expired.';
56
+ errorMessage += '\nTo fix this:';
57
+ errorMessage += '\n1. Go to your project Settings > Access Tokens';
58
+ errorMessage += '\n2. Check the expiration date of your AgileFlow Bot token';
59
+ errorMessage += '\n3. If expired, create a new token or extend the existing one';
60
+ } else if (res.statusCode === 403) {
61
+ errorMessage += '\n\nPermission denied. The AGILEFLOW_TOKEN needs "api" scope and maintainer role.';
62
+ errorMessage += '\nTo fix this:';
63
+ errorMessage += '\n1. Go to your project Settings > Access Tokens';
64
+ errorMessage += '\n2. Ensure your AgileFlow Bot token has "api" scope and maintainer role';
65
+ errorMessage += '\n3. If permissions are insufficient, create a new token with proper permissions';
66
+ }
67
+ } catch {
68
+ if (data) {
69
+ errorMessage += `\nResponse: ${data}`;
70
+ }
71
+ }
72
+ reject(new Error(errorMessage));
73
+ }
74
+ });
75
+ });
76
+
77
+ req.on('error', (error) => {
78
+ reject(new Error(`Network error: ${error.message}`));
79
+ });
80
+
81
+ req.write(postData);
82
+ req.end();
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Pushes a tag to GitLab via the API.
88
+ * Requires AGILEFLOW_TOKEN environment variable.
89
+ * Uses CI_SERVER_HOST and CI_PROJECT_PATH from GitLab CI environment.
90
+ * @param {string} tagName - The tag name
91
+ * @param {string} message - The tag message
92
+ * @returns {Promise<void>}
93
+ */
94
+ async function pushTag(tagName, message) {
95
+ const accessToken = process.env.AGILEFLOW_TOKEN;
96
+ const serverHost = process.env.CI_SERVER_HOST;
97
+ const projectPath = process.env.CI_PROJECT_PATH;
98
+ const commitSha = process.env.CI_COMMIT_SHA;
99
+
100
+ if (!accessToken) {
101
+ const projectUrl = serverHost && projectPath ? `https://${serverHost}/${projectPath}` : 'your-project';
102
+ const projectTokenUrl = `${projectUrl}/-/settings/access_tokens`;
103
+ const cicdUrl = `${projectUrl}/-/settings/ci_cd#js-cicd-variables-settings`;
104
+
105
+ throw new Error(
106
+ `AGILEFLOW_TOKEN environment variable is required but not set.\n\n` +
107
+ `To fix this:\n` +
108
+ `1. Create a project access token: ${projectTokenUrl}\n` +
109
+ ` - Name: AgileFlow Bot\n` +
110
+ ` - Role: maintainer\n` +
111
+ ` - Scopes: api\n` +
112
+ `2. Add it as a CI/CD variable: ${cicdUrl}\n` +
113
+ ` - Variable key: AGILEFLOW_TOKEN\n` +
114
+ ` - Protect variable: Yes (recommended)`
115
+ );
116
+ }
117
+
118
+ if (!serverHost) {
119
+ throw new Error('CI_SERVER_HOST environment variable is not set. Are you running inside GitLab CI?');
120
+ }
121
+
122
+ if (!projectPath) {
123
+ throw new Error('CI_PROJECT_PATH environment variable is not set. Are you running inside GitLab CI?');
124
+ }
125
+
126
+ if (!commitSha) {
127
+ throw new Error('CI_COMMIT_SHA environment variable is not set. Are you running inside GitLab CI?');
128
+ }
129
+
130
+ await createTagViaAPI(tagName, message || tagName, projectPath, serverHost, accessToken, commitSha);
131
+ }
132
+
133
+ module.exports = {
134
+ pushTag,
135
+ };
136
+