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
25 changes: 25 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Release Please

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: write
issues: write
pull-requests: write

concurrency:
group: release-please-${{ github.ref }}
cancel-in-progress: false

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
36 changes: 36 additions & 0 deletions .github/workflows/update-openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,46 @@ jobs:
- run: bun install --frozen-lockfile
- run: mise run spec:fetch
- run: mise run generate
- name: Guard OpenAPI update scope
run: |
changed_files="$(git status --porcelain --untracked-files=all)"
unexpected_files="$(printf '%s\n' "$changed_files" | awk '
NF {
path = substr($0, 4)
if ($0 ~ /^[RC]/) {
split(path, parts, " -> ")
path = parts[2]
}
if (path != "openapi/public.json" && path != "src/generated/commands.gen.ts") {
print path
}
}
')"
if [ -n "$unexpected_files" ]; then
echo "Unexpected files changed during OpenAPI update:"
printf '%s\n' "$unexpected_files"
exit 1
fi
- name: Detect OpenAPI changes
id: openapi-diff
run: |
if git diff --quiet -- openapi/public.json src/generated/commands.gen.ts; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "OpenAPI snapshot and generated command registry are unchanged."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "OpenAPI changes detected:"
git diff --stat -- openapi/public.json src/generated/commands.gen.ts
fi
- run: mise run check
if: steps.openapi-diff.outputs.changed == 'true'
- uses: peter-evans/create-pull-request@v8
if: steps.openapi-diff.outputs.changed == 'true'
with:
branch: chore/update-openapi
add-paths: |
openapi/public.json
src/generated/commands.gen.ts
commit-message: "chore: update OpenAPI spec and command registry"
title: "chore: update OpenAPI spec and command registry"
body: |
Expand Down
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.6.1"
}
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ release.
## Current Status

This scaffold establishes the architecture, packaging path, OpenAPI fetch task,
public operation registry generation, and output/error runtime contract. It does
not yet implement full API command execution.
public operation registry generation, release automation, and output/error
runtime contract. It does not yet implement full API command execution.

## Development

Expand Down Expand Up @@ -66,6 +66,26 @@ https://api.akua.dev/v1/openapi.json
The fetcher defaults to `AKUA_OPENAPI_URL` when set and rejects non-HTTPS
override URLs.

The scheduled `Update OpenAPI` workflow is idempotent: after fetching and
generating, it opens a pull request only when `openapi/public.json` or
`src/generated/commands.gen.ts` changed. The workflow fails if the update touches
any other tracked or untracked files.

## Release Automation

Release Please runs in manifest mode from `release-please-config.json` and
`.release-please-manifest.json`. It prepares release pull requests for the root
Bun package, updates package metadata, `CHANGELOG.md`, and the `akua --version`
marker in `src/bin/akua.ts`, and creates `v*` version tags and GitHub releases
after release PRs merge.

The workflow uses `secrets.RELEASE_PLEASE_TOKEN` so release-created tags can
trigger the tag-based release workflow.

The separate tag-triggered release workflow builds and uploads the Linux x64
binary artifact. The Release Please config does not add npm publishing or expand
binary publishing behavior.

## Runtime Contract

Default output is adaptive:
Expand Down
32 changes: 28 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ scripts/generate-commands.ts operationId-driven command registry generator
src/bin/akua.ts executable entrypoint
src/runtime/ output, errors, exit codes, command contracts
src/generated/commands.gen.ts generated public command registry
.github/workflows/update-openapi.yml
idempotent public OpenAPI update automation
.github/workflows/release-please.yml
release PR, tag, and GitHub release automation
.github/workflows/release.yml tag-triggered binary artifact build
release-please-config.json Release Please manifest-mode config
.release-please-manifest.json Release Please root package version manifest
docs/architecture.md this spec
test/ Bun tests for scaffold contracts
```
Expand Down Expand Up @@ -78,9 +85,13 @@ mise run generate:check # fails on drift

`mise run spec:fetch` defaults to `AKUA_OPENAPI_URL`, which is set to the
production source in `mise.toml`, and `scripts/fetch-openapi.ts` also accepts an
explicit URL argument. The scheduled `Update OpenAPI` workflow runs weekly and
opens a pull request after fetching the snapshot, regenerating the registry, and
running `mise run check`.
explicit URL argument. The scheduled `Update OpenAPI` workflow runs weekly,
fetches the snapshot, regenerates the registry, and then fails if tracked or
untracked files outside `openapi/public.json` and
`src/generated/commands.gen.ts` changed. It is idempotent when those files match
the repository: unchanged runs report a no-op and do not run `mise run check` or
open/update a pull request. Changed runs execute `mise run check` and open or
update a pull request containing only the snapshot and generated registry.

## API, Auth, And Config Model

Expand Down Expand Up @@ -233,6 +244,18 @@ The local task `mise run build:binary` compiles a host binary at `dist/akua`.
The initial release workflow builds a Linux x64 artifact; macOS and Windows
matrix targets should be added once the scaffold is validated on CI.

Release Please runs in manifest mode for the root Bun package. It uses
`release-please-config.json` and `.release-please-manifest.json` to prepare
release PRs, update package metadata and `CHANGELOG.md`, keep the
`src/bin/akua.ts` `x-release-please-version` marker aligned with
`akua --version`, create `v*` version tags without a component prefix, and
create GitHub releases after release PRs merge. The workflow uses
`secrets.RELEASE_PLEASE_TOKEN` instead of the default `GITHUB_TOKEN` so
release-created tags can trigger the tag-based binary workflow. The config
deliberately omits npm publishing and does not expand the binary publishing
surface; the existing tag-triggered release workflow remains responsible for
uploading the Linux x64 binary artifact.

## Testing Strategy

Current tests cover:
Expand All @@ -242,7 +265,8 @@ Current tests cover:
- agent and JSON rendering;
- structured error payloads;
- OpenAPI fetch guard and document shape validation;
- public-only operation collection.
- public-only operation collection;
- Release Please config, manifest, token, and CLI version marker validation.

Current validation also runs `mise run generate:check` to catch generated
registry drift.
Expand Down
16 changes: 16 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"include-component-in-tag": false,
"packages": {
".": {
"release-type": "node",
"package-name": "@akua-dev/cli",
"changelog-path": "CHANGELOG.md",
"extra-files": [
{
"type": "generic",
"path": "src/bin/akua.ts"
}
]
}
}
}
2 changes: 1 addition & 1 deletion src/bin/akua.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AkuaCliError, commandNotImplemented, usageError } from "../runtime/erro
import { detectOutputMode, type OutputMode } from "../runtime/mode";
import { renderError, renderSuccess, type RenderEnvelope } from "../runtime/render";

const VERSION = "0.0.0";
const VERSION = "0.0.0"; // x-release-please-version

export async function main(argv = process.argv.slice(2), env = process.env): Promise<number> {
let mode: OutputMode = fallbackErrorMode(argv);
Expand Down
55 changes: 55 additions & 0 deletions test/release-please-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";

interface ReleasePleaseConfig {
"include-component-in-tag"?: boolean;
packages?: Record<string, {
"release-type"?: string;
"package-name"?: string;
"changelog-path"?: string;
"extra-files"?: Array<{
type?: string;
path?: string;
}>;
}>;
}

describe("release-please configuration", () => {
test("configures the root Bun CLI package without publish automation", () => {
const config = JSON.parse(readFileSync("release-please-config.json", "utf8")) as ReleasePleaseConfig;

expect(config.packages?.["."]).toEqual({
"release-type": "node",
"package-name": "@akua-dev/cli",
"changelog-path": "CHANGELOG.md",
"extra-files": [
{
type: "generic",
path: "src/bin/akua.ts",
},
],
});
expect(config["include-component-in-tag"]).toBe(false);
expect(JSON.stringify(config)).not.toContain("npm");
expect(JSON.stringify(config)).not.toContain("publish");
});

test("updates the CLI version reported by akua --version", () => {
const cli = readFileSync("src/bin/akua.ts", "utf8");

expect(cli).toMatch(/const VERSION = "\d+\.\d+\.\d+(?:[-+][^"]+)?"; \/\/ x-release-please-version/);
});

test("tracks the root package release version", () => {
const manifest = JSON.parse(readFileSync(".release-please-manifest.json", "utf8")) as Record<string, string>;

expect(Object.keys(manifest)).toEqual(["."]);
expect(manifest["."]).toMatch(/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/);
});

test("uses an explicit token so release PRs and tags trigger follow-up workflows", () => {
const workflow = readFileSync(".github/workflows/release-please.yml", "utf8");

expect(workflow).toContain("token: ${{ secrets.RELEASE_PLEASE_TOKEN }}");
});
});
Loading