diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a8059a..ed3d892 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ -# CI workflow for stackctl. -# Runs on every push and PR to main/dev branches. -# Validates format, linting, type checking, tests, and coverage. +/** + * CI workflow for stackctl. + * + * Runs on every push and PR to main/dev branches. + * Validates format, linting, type checking, tests, and coverage. + */ name: CI on: @@ -36,16 +39,19 @@ jobs: - name: Type check run: deno task check - - name: Run tests with coverage - run: deno task test --coverage=.coverage + - name: Run tests + run: deno task test - name: Generate coverage report if: success() - run: deno task coverage + run: | + deno test --allow-read --allow-write --allow-env --allow-run --allow-sys --coverage=.coverage + deno coverage --detailed .coverage build: runs-on: ubuntu-latest needs: check + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' steps: - name: Checkout uses: actions/checkout@v4 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d9f412d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,71 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at anitrendapp@gmail.com. The project +team will review and investigate all complaints, and will respond in a way that +it deems appropriate to the circumstances. The project team is obligated to +maintain confidentiality with regard to the reporter of an incident. Further +details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at [http://contributor-covenant.org/version/1/4][version]. + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..01fbffe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,85 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via a GitHub issue, email, or Discord with the owners of this repository before making a change. + +Please note we have a [Code of Conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. + +## Issue Guidelines + +- Search existing issues for duplicates before creating a new one. +- Keep individual issues for each suggestion, bug, or feature. +- Titles should use a scoped prefix for visibility, for example `[config] Harden profile merge order` or `[generate] Port idempotent stack generation`. +- Use labels for taxonomy such as `feature`, `bug`, `refactor`, or `docs` rather than encoding taxonomy into the title itself. + +## Pull Request Guidelines + +- Make individual pull requests for each issue, and link the issue in the PR description. +- PR titles follow the format `(): ` and stay aligned with the branch intent. +- Do not stage files excluded by `.gitignore`. +- Commits should reference relevant issues or other PRs. +- Automated pull requests should follow the same branch naming conventions as contributor PRs. + +## Quality Standards + +Exhaustive unit tests are mandatory for all PRs, demonstrating the cases guarded against and extent of coverage. + +### Branch Naming + +``` +/- +``` + +When no issue number exists yet, use `/` temporarily and link the branch to the issue as soon as it is created. + +Supported types: + +- `feat` -- A new feature +- `fix` -- A bug fix +- `chore` -- Routine tasks, dependencies, and maintenance +- `docs` -- Documentation only changes +- `refactor` -- Code change that neither fixes a bug nor adds a feature +- `test` -- Adding missing or correcting existing tests +- `build` -- Changes that affect the build system or dependencies +- `ci` -- Changes to CI configuration or automation scripts +- `revert` -- Reverting a previous commit + +Examples: + +- `feat/1208-implement-override-merging` +- `fix/1177-template-drift-in-config-init` +- `docs/1234-update-contributing-guidelines` +- `ci/update-deno-version` + +### Commit Messages + +``` +(): +``` + +Scope should be a module area such as `config`, `generate`, `render`, `cli`, `docker`, `secrets`, or `project`. + +Examples: + +- `feat(config): add profile overlay discovery` +- `fix(render): handle nested $VAR without braces` +- `chore(project): pin Deno version in CI` +- `docs(contributing): clarify PR title format` + +### Pull Request Titles + +Use the same format `(): ` and keep the title consistent with the branch intent. + +### Before Submitting + +- Run `deno task check` (fmt, lint, type-check) and fix any issues. +- Run `deno task test` and confirm all tests pass (existing and new). +- Run `deno task coverage` and verify coverage targets are met (minimum 80% line coverage for `src/`). +- Verify that no secrets, credentials, or local paths were committed. + +## Code of Conduct + +See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for our community standards and enforcement policies. + +--- + +Thank you for your contribution! diff --git a/README.md b/README.md index adfd243..ddbdab7 100644 --- a/README.md +++ b/README.md @@ -1 +1,94 @@ -# stackctl \ No newline at end of file +# stackctl + +[![CI](https://github.com/AniTrend/stackctl/actions/workflows/ci.yml/badge.svg)](https://github.com/AniTrend/stackctl/actions/workflows/ci.yml) + +A Deno-powered CLI for managing local Docker Compose stacks across multi-service repositories, with config-driven profiles, overrides, secrets, and render pipelines. + +Status: **Early development** -- all 15 feature issues are planned and tracked on the [issue tracker](https://github.com/AniTrend/stackctl/issues). + +--- + +## Commands + +| Command | Status | Description | +|---|---|---| +| `stackctl init` | Planned | Generate commented `.stackctl` config | +| `stackctl generate` | Planned | Generate `stacks/*.yml` from per-service sources | +| `stackctl render` | Planned | Resolve `${VAR}` placeholders in stack files | +| `stackctl overrides` | Planned | Profile and explicit override merging | +| `stackctl up` | Planned | Deploy stacks to Docker Swarm | +| `stackctl down` | Planned | Tear down stacks | +| `stackctl status` | Planned | Show service status | +| `stackctl logs` | Planned | Follow container logs | +| `stackctl sync` | Planned | Sync images and volumes | +| `stackctl doctor` | Planned | Validate environment | +| `stackctl reload` | Planned | Re-render and reconcile without teardown | +| `stackctl secrets` | Planned | Encrypt/decrypt/deploy/clean/check with SOPS+age | +| `stackctl env` | Planned | Scaffold `.env` files from examples | +| `stackctl plan` | Planned | Dry-run summary of all operations | +| `stackctl completions` | Planned | Generate shell completions (bash/zsh/fish) | + +--- + +## Quick Start + +```bash +# Install (once released) +deno install -f --allow-run --allow-env --allow-read --allow-write \ + -n stackctl jsr:@anitrend/stackctl + +# Initialize a config +stackctl init + +# Generate stacks from service sources +stackctl generate + +# See what would happen +stackctl plan +``` + +--- + +## Development + +### Prerequisites + +- [Deno 2.x](https://deno.com) (2.8.0+) + +### Setup + +```bash +git clone git@github.com:AniTrend/stackctl.git +cd stackctl + +# Run tests +deno task test + +# Run checks +deno task check + +# Build a binary +deno task build +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for branch naming, commit conventions, and PR guidelines. + +--- + +## License + +``` +Copyright 2026 AniTrend + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/deno.json b/deno.json index 2327387..d4e3782 100644 --- a/deno.json +++ b/deno.json @@ -8,13 +8,13 @@ "fmt": "deno fmt", "fmt:check": "deno fmt --check", "lint": "deno lint", - "test": "deno test", + "test": "deno test --allow-read --allow-write --allow-env --allow-run --allow-sys", "coverage": "deno coverage --detailed", - "build": "deno compile --allow-read --allow-write --allow-env --allow-sys --allow-run=git,docker,sops,age,age-keygen,shred,rm --output dist/stackctl src/main.ts", - "build:darwin:x64": "deno compile --target x86_64-apple-darwin --allow-read --allow-write --allow-env --allow-sys --allow-run=git,docker,sops,age,age-keygen,shred,rm --output dist/stackctl-darwin-x64 src/main.ts", - "build:darwin:arm64": "deno compile --target aarch64-apple-darwin --allow-read --allow-write --allow-env --allow-sys --allow-run=git,docker,sops,age,age-keygen,shred,rm --output dist/stackctl-darwin-arm64 src/main.ts", - "build:linux:x64": "deno compile --target x86_64-unknown-linux-gnu --allow-read --allow-write --allow-env --allow-sys --allow-run=git,docker,sops,age,age-keygen,shred,rm --output dist/stackctl-linux-x64 src/main.ts", - "build:linux:arm64": "deno compile --target aarch64-unknown-linux-gnu --allow-read --allow-write --allow-env --allow-sys --allow-run=git,docker,sops,age,age-keygen,shred,rm --output dist/stackctl-linux-arm64 src/main.ts" + "build": "deno compile --output dist/stackctl src/main.ts", + "build:darwin:x64": "deno compile --target x86_64-apple-darwin --output dist/stackctl-darwin-x64 src/main.ts", + "build:darwin:arm64": "deno compile --target aarch64-apple-darwin --output dist/stackctl-darwin-arm64 src/main.ts", + "build:linux:x64": "deno compile --target x86_64-unknown-linux-gnu --output dist/stackctl-linux-x64 src/main.ts", + "build:linux:arm64": "deno compile --target aarch64-unknown-linux-gnu --output dist/stackctl-linux-arm64 src/main.ts" }, "imports": { "@cliffy/command": "jsr:@cliffy/command@^1.0.0", diff --git a/src/cli/mod.ts b/src/cli/mod.ts index 12593ae..2b815fe 100644 --- a/src/cli/mod.ts +++ b/src/cli/mod.ts @@ -34,6 +34,7 @@ export function buildCli(): Command { // Default action: show help when no subcommand matches cli.action(() => { cli.showHelp(); + Deno.exit(0); }); // --- init (issue #3) --- @@ -45,7 +46,8 @@ export function buildCli(): Command { .option("--force", "Overwrite existing .stackctl file.") .option("--dry-run", "Print the config that would be written without writing.") .action(() => { - throw new Error("init: not yet implemented (issue #3)"); + console.error("init: not yet implemented (issue #3)"); + Deno.exit(1); }); // --- generate (issue #4) --- @@ -55,7 +57,8 @@ export function buildCli(): Command { .option("--output-dir ", "Write generated stacks to a specific directory.") .option("--profile ", "Use a specific profile.") .action(() => { - throw new Error("generate: not yet implemented (issue #4)"); + console.error("generate: not yet implemented (issue #4)"); + Deno.exit(1); }); // --- render (issue #5) --- @@ -72,7 +75,8 @@ export function buildCli(): Command { "Comma-separated list of override files to apply before rendering.", ) .action(() => { - throw new Error("render: not yet implemented (issue #5)"); + console.error("render: not yet implemented (issue #5)"); + Deno.exit(1); }); // --- up (issue #6) --- @@ -85,7 +89,8 @@ export function buildCli(): Command { .option("--profile ", "Use a specific profile.") .option("--override ", "Comma-separated list of override files.") .action(() => { - throw new Error("up: not yet implemented (issue #6)"); + console.error("up: not yet implemented (issue #6)"); + Deno.exit(1); }); // --- down (issue #6) --- @@ -96,7 +101,8 @@ export function buildCli(): Command { .option("--stacks ", "Comma-separated list of stack names to remove.") .option("--profile ", "Use a specific profile.") .action(() => { - throw new Error("down: not yet implemented (issue #6)"); + console.error("down: not yet implemented (issue #6)"); + Deno.exit(1); }); // --- status (issue #6) --- @@ -105,7 +111,8 @@ export function buildCli(): Command { .option("--stacks ", "Comma-separated list of stack names.") .option("--profile ", "Use a specific profile.") .action(() => { - throw new Error("status: not yet implemented (issue #6)"); + console.error("status: not yet implemented (issue #6)"); + Deno.exit(1); }); // --- logs (issue #6) --- @@ -114,7 +121,8 @@ export function buildCli(): Command { .option("--stacks ", "Comma-separated list of stack names.") .option("--profile ", "Use a specific profile.") .action(() => { - throw new Error("logs: not yet implemented (issue #6)"); + console.error("logs: not yet implemented (issue #6)"); + Deno.exit(1); }); // --- sync (issue #6) --- @@ -123,7 +131,8 @@ export function buildCli(): Command { .option("--non-interactive", "Skip confirmation; exit 1 on drift.") .option("--profile ", "Use a specific profile.") .action(() => { - throw new Error("sync: not yet implemented (issue #6)"); + console.error("sync: not yet implemented (issue #6)"); + Deno.exit(1); }); // --- doctor (issue #6) --- @@ -132,7 +141,8 @@ export function buildCli(): Command { .option("--check-secrets", "Also check for secrets tooling (sops, age).") .option("--profile ", "Use a specific profile.") .action(() => { - throw new Error("doctor: not yet implemented (issue #6)"); + console.error("doctor: not yet implemented (issue #6)"); + Deno.exit(1); }); // --- reload (issue #9) --- @@ -145,7 +155,8 @@ export function buildCli(): Command { .option("--override ", "Comma-separated list of override files.") .option("--dry-run", "Print planned actions without executing.") .action(() => { - throw new Error("reload: not yet implemented (issue #9)"); + console.error("reload: not yet implemented (issue #9)"); + Deno.exit(1); }); // --- secrets (issue #7) --- @@ -155,32 +166,37 @@ export function buildCli(): Command { .option("--profile ", "Use a specific profile.") .option("--dry-run", "Print planned actions without executing.") .action(() => { - throw new Error("secrets encrypt: not yet implemented (issue #7)"); + console.error("secrets encrypt: not yet implemented (issue #7)"); + Deno.exit(1); }); secretsCmd.command("decrypt", "Decrypt encrypted .env files to plaintext.") .arguments("[services...:string]") .option("--profile ", "Use a specific profile.") .option("--dry-run", "Print planned actions without executing.") .action(() => { - throw new Error("secrets decrypt: not yet implemented (issue #7)"); + console.error("secrets decrypt: not yet implemented (issue #7)"); + Deno.exit(1); }); secretsCmd.command("deploy", "Decrypt and deploy stacks with secret values.") .arguments("[services...:string]") .option("--profile ", "Use a specific profile.") .option("--dry-run", "Print planned actions without executing.") .action(() => { - throw new Error("secrets deploy: not yet implemented (issue #7)"); + console.error("secrets deploy: not yet implemented (issue #7)"); + Deno.exit(1); }); secretsCmd.command("clean", "Remove plaintext .env files that have encrypted counterparts.") .option("--profile ", "Use a specific profile.") .option("--dry-run", "Print planned actions without executing.") .action(() => { - throw new Error("secrets clean: not yet implemented (issue #7)"); + console.error("secrets clean: not yet implemented (issue #7)"); + Deno.exit(1); }); secretsCmd.command("check", "Check secrets tooling availability.") .option("--profile ", "Use a specific profile.") .action(() => { - throw new Error("secrets check: not yet implemented (issue #7)"); + console.error("secrets check: not yet implemented (issue #7)"); + Deno.exit(1); }); // --- env (issue #14) --- @@ -195,7 +211,8 @@ export function buildCli(): Command { .option("--from-profile ", "Materialize env from a profile preset.") .option("--materialize", "Materialize profile preset env values.") .action(() => { - throw new Error("env: not yet implemented (issue #14)"); + console.error("env: not yet implemented (issue #14)"); + Deno.exit(1); }); // --- plan (issue #15) --- @@ -206,22 +223,26 @@ export function buildCli(): Command { .option("--override ", "Comma-separated list of override files.") .option("--json", "Output machine-readable JSON.") .action(() => { - throw new Error("plan: not yet implemented (issue #15)"); + console.error("plan: not yet implemented (issue #15)"); + Deno.exit(1); }); // --- completions (issue #10) --- const completionsCmd = cli.command("completions", "Generate shell completion scripts."); completionsCmd.command("bash", "Generate bash completion script.") .action(() => { - throw new Error("completions bash: not yet implemented (issue #10)"); + console.error("completions bash: not yet implemented (issue #10)"); + Deno.exit(1); }); completionsCmd.command("zsh", "Generate zsh completion script.") .action(() => { - throw new Error("completions zsh: not yet implemented (issue #10)"); + console.error("completions zsh: not yet implemented (issue #10)"); + Deno.exit(1); }); completionsCmd.command("fish", "Generate fish completion script.") .action(() => { - throw new Error("completions fish: not yet implemented (issue #10)"); + console.error("completions fish: not yet implemented (issue #10)"); + Deno.exit(1); }); return cli as unknown as Command;