Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions build/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52151,8 +52151,8 @@ class ProjectRepository {
const { data: release } = await octokit.rest.repos.createRelease({
owner,
repo,
tag_name: `v${version}`,
name: `v${version} - ${title}`,
tag_name: version,
name: `${version} - ${title}`,
body: changelog,
draft: false,
prerelease: false,
Expand Down Expand Up @@ -53833,6 +53833,25 @@ const project_repository_1 = __nccwpck_require__(7917);
const constants_1 = __nccwpck_require__(8593);
const logger_1 = __nccwpck_require__(8836);
const task_emoji_1 = __nccwpck_require__(9785);
/** Semantic version pattern: x, x.y, or x.y.z (digits only, no leading 'v'). */
const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/;
function normalizeAndValidateVersion(version) {
const trimmed = version.trim();
const withoutV = trimmed.startsWith("v") ? trimmed.slice(1).trim() : trimmed;
if (withoutV.length === 0) {
return {
valid: false,
error: `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0).`,
};
}
if (!SEMVER_PATTERN.test(withoutV)) {
return {
valid: false,
error: `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0). Got: ${version}`,
};
}
return { valid: true, normalized: withoutV };
}
class CreateReleaseUseCase {
constructor() {
this.taskId = 'CreateReleaseUseCase';
Expand Down Expand Up @@ -53863,6 +53882,7 @@ class CreateReleaseUseCase {
`${constants_1.INPUT_KEYS.SINGLE_ACTION_TITLE} is not set.`
],
}));
return result;
}
else if (param.singleAction.changelog.length === 0) {
(0, logger_1.logError)(`Changelog is not set.`);
Expand All @@ -53874,9 +53894,22 @@ class CreateReleaseUseCase {
`${constants_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`
],
}));
return result;
}
const versionCheck = normalizeAndValidateVersion(param.singleAction.version);
if (!versionCheck.valid) {
(0, logger_1.logError)(versionCheck.error);
result.push(new result_1.Result({
id: this.taskId,
success: false,
executed: true,
errors: [versionCheck.error],
}));
return result;
}
const releaseVersion = `v${versionCheck.normalized}`;
try {
const releaseUrl = await this.projectRepository.createRelease(param.owner, param.repo, param.singleAction.version, param.singleAction.title, param.singleAction.changelog, param.tokens.token);
const releaseUrl = await this.projectRepository.createRelease(param.owner, param.repo, releaseVersion, param.singleAction.title, param.singleAction.changelog, param.tokens.token);
if (releaseUrl) {
result.push(new result_1.Result({
id: this.taskId,
Expand All @@ -53886,7 +53919,7 @@ class CreateReleaseUseCase {
}));
}
else {
(0, logger_1.logWarn)(`CreateRelease: createRelease returned no URL for version ${param.singleAction.version}.`);
(0, logger_1.logWarn)(`CreateRelease: createRelease returned no URL for version ${releaseVersion}.`);
result.push(new result_1.Result({
id: this.taskId,
success: false,
Expand Down Expand Up @@ -53961,24 +53994,25 @@ class CreateTagUseCase {
}));
return result;
}
const tagName = `v${param.singleAction.version}`;
try {
const sha1Tag = await this.projectRepository.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, param.singleAction.version, param.tokens.token);
const sha1Tag = await this.projectRepository.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, tagName, param.tokens.token);
if (sha1Tag) {
result.push(new result_1.Result({
id: this.taskId,
success: true,
executed: true,
steps: [`Tag ${param.singleAction.version} is ready: ${sha1Tag}`],
steps: [`Tag ${tagName} is ready: ${sha1Tag}`],
}));
}
else {
(0, logger_1.logWarn)(`CreateTag: createTag returned no SHA for version ${param.singleAction.version}.`);
(0, logger_1.logWarn)(`CreateTag: createTag returned no SHA for version ${tagName}.`);
result.push(new result_1.Result({
id: this.taskId,
success: false,
executed: true,
errors: [
`Failed to create tag ${param.singleAction.version}.`
`Failed to create tag ${tagName}.`
],
}));
}
Expand All @@ -53989,7 +54023,7 @@ class CreateTagUseCase {
id: this.taskId,
success: false,
executed: true,
steps: [`Failed to create tag ${param.singleAction.version}.`],
steps: [`Failed to create tag ${tagName}.`],
errors: [
JSON.stringify(error)
],
Expand Down
52 changes: 43 additions & 9 deletions build/github_action/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47232,8 +47232,8 @@ class ProjectRepository {
const { data: release } = await octokit.rest.repos.createRelease({
owner,
repo,
tag_name: `v${version}`,
name: `v${version} - ${title}`,
tag_name: version,
name: `${version} - ${title}`,
body: changelog,
draft: false,
prerelease: false,
Expand Down Expand Up @@ -48914,6 +48914,25 @@ const project_repository_1 = __nccwpck_require__(7917);
const constants_1 = __nccwpck_require__(8593);
const logger_1 = __nccwpck_require__(8836);
const task_emoji_1 = __nccwpck_require__(9785);
/** Semantic version pattern: x, x.y, or x.y.z (digits only, no leading 'v'). */
const SEMVER_PATTERN = /^\d+(\.\d+){0,2}$/;
function normalizeAndValidateVersion(version) {
const trimmed = version.trim();
const withoutV = trimmed.startsWith("v") ? trimmed.slice(1).trim() : trimmed;
if (withoutV.length === 0) {
return {
valid: false,
error: `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0).`,
};
}
if (!SEMVER_PATTERN.test(withoutV)) {
return {
valid: false,
error: `${constants_1.INPUT_KEYS.SINGLE_ACTION_VERSION} must be a semantic version (e.g. 1.0.0). Got: ${version}`,
};
}
return { valid: true, normalized: withoutV };
}
class CreateReleaseUseCase {
constructor() {
this.taskId = 'CreateReleaseUseCase';
Expand Down Expand Up @@ -48944,6 +48963,7 @@ class CreateReleaseUseCase {
`${constants_1.INPUT_KEYS.SINGLE_ACTION_TITLE} is not set.`
],
}));
return result;
}
else if (param.singleAction.changelog.length === 0) {
(0, logger_1.logError)(`Changelog is not set.`);
Expand All @@ -48955,9 +48975,22 @@ class CreateReleaseUseCase {
`${constants_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`
],
}));
return result;
}
const versionCheck = normalizeAndValidateVersion(param.singleAction.version);
if (!versionCheck.valid) {
(0, logger_1.logError)(versionCheck.error);
result.push(new result_1.Result({
id: this.taskId,
success: false,
executed: true,
errors: [versionCheck.error],
}));
return result;
}
const releaseVersion = `v${versionCheck.normalized}`;
try {
const releaseUrl = await this.projectRepository.createRelease(param.owner, param.repo, param.singleAction.version, param.singleAction.title, param.singleAction.changelog, param.tokens.token);
const releaseUrl = await this.projectRepository.createRelease(param.owner, param.repo, releaseVersion, param.singleAction.title, param.singleAction.changelog, param.tokens.token);
if (releaseUrl) {
result.push(new result_1.Result({
id: this.taskId,
Expand All @@ -48967,7 +49000,7 @@ class CreateReleaseUseCase {
}));
}
else {
(0, logger_1.logWarn)(`CreateRelease: createRelease returned no URL for version ${param.singleAction.version}.`);
(0, logger_1.logWarn)(`CreateRelease: createRelease returned no URL for version ${releaseVersion}.`);
result.push(new result_1.Result({
id: this.taskId,
success: false,
Expand Down Expand Up @@ -49042,24 +49075,25 @@ class CreateTagUseCase {
}));
return result;
}
const tagName = `v${param.singleAction.version}`;
try {
const sha1Tag = await this.projectRepository.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, param.singleAction.version, param.tokens.token);
const sha1Tag = await this.projectRepository.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, tagName, param.tokens.token);
if (sha1Tag) {
result.push(new result_1.Result({
id: this.taskId,
success: true,
executed: true,
steps: [`Tag ${param.singleAction.version} is ready: ${sha1Tag}`],
steps: [`Tag ${tagName} is ready: ${sha1Tag}`],
}));
}
else {
(0, logger_1.logWarn)(`CreateTag: createTag returned no SHA for version ${param.singleAction.version}.`);
(0, logger_1.logWarn)(`CreateTag: createTag returned no SHA for version ${tagName}.`);
result.push(new result_1.Result({
id: this.taskId,
success: false,
executed: true,
errors: [
`Failed to create tag ${param.singleAction.version}.`
`Failed to create tag ${tagName}.`
],
}));
}
Expand All @@ -49070,7 +49104,7 @@ class CreateTagUseCase {
id: this.taskId,
success: false,
executed: true,
steps: [`Failed to create tag ${param.singleAction.version}.`],
steps: [`Failed to create tag ${tagName}.`],
errors: [
JSON.stringify(error)
],
Expand Down
4 changes: 2 additions & 2 deletions docs/features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ When you set `single-action` (and, when required, `single-action-issue`, `single
| **`think_action`** | — | Uses OpenCode Plan for deep code analysis and change proposals (reasoning over the codebase). No issue required. |
| **`initial_setup`** | — | Performs initial setup steps (e.g. for repo or project). No issue required. |
| **`create_release`** | `single-action-version`, `single-action-title`, `single-action-changelog` | Creates a GitHub release with the given version, title, and changelog. |
| **`create_tag`** | `single-action-version` | Creates a Git tag for the given version. |
| **`publish_github_action`** | | Publishes or updates the GitHub Action (e.g. versioning, release). |
| **`create_tag`** | `single-action-version` | Creates a Git tag with prefix `v` (e.g. `v1.2.0`) for the given version from the release branch. |
| **`publish_github_action`** | `single-action-version` | Publishes or updates the GitHub Action: creates/updates the major version tag (e.g. `v2` from `v2.0.4`). Requires `create_tag` to have been run first. |
| **`deployed_action`** | `single-action-issue` | Marks the issue as deployed; updates labels and project state (e.g. "deployed"). |

Single actions that **throw an error** if the last step fails: `publish_github_action`, `create_release`, `deployed_action`, `create_tag`. This lets the workflow fail the job when the action does not succeed.
Expand Down
6 changes: 3 additions & 3 deletions docs/single-actions/available-actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ These actions need **`single-action-issue`** set to the issue number. The workfl
| **`think_action`** | — | **Deep code analysis** and change proposals (OpenCode Plan). You can pass a question (e.g. from CLI with `-q "..."`). No issue required. | One-off reasoning over the codebase; use from CLI or a workflow that provides context. |
| **`initial_setup`** | — | Performs **initial setup** steps: creates labels, issue types (if supported), verifies access. No issue required. | First-time repo setup; run once or when you add new labels/types. |
| **`create_release`** | `single-action-version`, `single-action-title`, `single-action-changelog` | Creates a **GitHub release** with the given version, title, and changelog (markdown body). | From a workflow after tests pass; use version and changelog from your build or inputs. |
| **`create_tag`** | `single-action-version` | Creates a **Git tag** for the given version. | When you only need a tag (e.g. for versioning) without a full release. |
| **`publish_github_action`** | | **Publishes or updates** the GitHub Action (e.g. versioning, release to marketplace). No issue required. | In a CI job that builds and publishes the action. |
| **`create_tag`** | `single-action-version` | Creates a **Git tag** with prefix `v` (e.g. `v1.2.3`) for the given version from the release branch. | When you only need a tag (e.g. for versioning) without a full release. The tag is created from the `releaseBranch` stored in issue configuration. |
| **`publish_github_action`** | `single-action-version` | **Publishes or updates** the GitHub Action: creates/updates the major version tag (e.g. `v2` from `v2.0.4`) and the corresponding GitHub Release. Requires that `create_tag` has been run first to create the source tag `v{version}`. | In a CI job that builds and publishes the action, after `create_tag` and `create_release` have run. |

## Actions that fail the job on failure

Expand Down Expand Up @@ -55,7 +55,7 @@ The **`copilot`** CLI command (e.g. `giik copilot -p "..."`) uses the OpenCode *
| `initial_setup` | — | — | — | — |
| `create_release` | — | ✅ | ✅ | ✅ |
| `create_tag` | — | ✅ | — | — |
| `publish_github_action` | — | | — | — |
| `publish_github_action` | — | | — | — |

## Next steps

Expand Down
5 changes: 4 additions & 1 deletion docs/single-actions/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,19 @@ Changelog can be read from a file or generated in a previous step and passed as

## Workflow: create tag

Create only a tag (no release body):
Create a Git tag with prefix `v` (e.g. `v1.2.0`) from the release branch:

```yaml
- uses: vypdev/copilot@v2
with:
token: ${{ secrets.PAT }}
single-action: create_tag
single-action-version: "1.2.0"
single-action-issue: "100" # Issue with releaseBranch in configuration
```

**Note:** The tag is created from the `releaseBranch` stored in the issue configuration. The version input `1.2.0` will create tag `v1.2.0`.

## Workflow: deployed

Mark issue `100` as deployed (e.g. from your release workflow):
Expand Down
2 changes: 1 addition & 1 deletion src/data/repository/__tests__/project_repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ describe("ProjectRepository.createRelease", () => {
const result = await repo.createRelease(
"owner",
"repo",
"1.0",
"v1.0",
"First release",
"Changelog",
"token"
Expand Down
4 changes: 2 additions & 2 deletions src/data/repository/project_repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,8 +758,8 @@ export class ProjectRepository {
const { data: release } = await octokit.rest.repos.createRelease({
owner,
repo,
tag_name: `v${version}`,
name: `v${version} - ${title}`,
tag_name: version,
name: `${version} - ${title}`,
body: changelog,
draft: false,
prerelease: false,
Expand Down
28 changes: 27 additions & 1 deletion src/usecase/actions/__tests__/create_release_use_case.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ describe('CreateReleaseUseCase', () => {
expect(results.some((r) => r.errors?.some((e) => String(e).includes(`${INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`)))).toBe(true);
});

it('returns failure when version format is invalid', async () => {
const param = baseParam({
singleAction: { version: 'abc', title: 'Release', changelog: '- Fix' },
});
const results = await useCase.invoke(param);
expect(results).toHaveLength(1);
expect(results[0].success).toBe(false);
expect(results[0].errors?.some((e) => String(e).includes(INPUT_KEYS.SINGLE_ACTION_VERSION))).toBe(true);
expect(mockCreateRelease).not.toHaveBeenCalled();
});

it('accepts version with leading v and produces tag v1.0.0 (no double v)', async () => {
mockCreateRelease.mockResolvedValue('https://github.com/owner/repo/releases/tag/v1.0.0');
const param = baseParam({ singleAction: { version: 'v1.0.0', title: 'Release', changelog: '- Fix' } });
const results = await useCase.invoke(param);
expect(results[0].success).toBe(true);
expect(mockCreateRelease).toHaveBeenCalledWith(
'owner',
'repo',
'v1.0.0',
'Release',
'- Fix',
'token'
);
});

it('returns success with release URL when createRelease succeeds', async () => {
mockCreateRelease.mockResolvedValue('https://github.com/owner/repo/releases/tag/v1.0.0');
const param = baseParam();
Expand All @@ -76,7 +102,7 @@ describe('CreateReleaseUseCase', () => {
expect(mockCreateRelease).toHaveBeenCalledWith(
'owner',
'repo',
'1.0.0',
'v1.0.0',
'Release title',
'- Fix bug',
'token'
Expand Down
4 changes: 2 additions & 2 deletions src/usecase/actions/__tests__/create_tag_use_case.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ describe('CreateTagUseCase', () => {
expect(results).toHaveLength(1);
expect(results[0]).toBeInstanceOf(Result);
expect(results[0].success).toBe(true);
expect(results[0].steps?.some((s) => s.includes('1.0.0') && s.includes('abc123'))).toBe(true);
expect(results[0].steps?.some((s) => s.includes('v1.0.0') && s.includes('abc123'))).toBe(true);
expect(mockCreateTag).toHaveBeenCalledWith(
'owner',
'repo',
'release/1.0.0',
'1.0.0',
'v1.0.0',
'token'
);
});
Expand Down
Loading
Loading