From 92f02cc6959a41e4af40db96e5b2fc394748fe6c Mon Sep 17 00:00:00 2001 From: Agents Agent Date: Mon, 29 Jun 2026 09:11:58 +0000 Subject: [PATCH] feat: add deploy automation reusables --- .gitattributes | 6 + .github/CODEOWNERS | 2 +- .github/workflows/add-to-project.yml | 61 +++ .github/workflows/container-publish.yml | 2 +- .github/workflows/deploy-bundle.yml | 121 +++++ .github/workflows/deploy-sources-render.yml | 104 ++++ .github/workflows/publish-api-clients.yml | 2 +- .../workflows/repository-hygiene-guard.yml | 104 ++++ .gitignore | 12 + .specify/.agent-kit-sdd-seed.sha256 | 10 - .specify/memory/constitution.md | 54 -- .specify/scripts/bash/check-prerequisites.sh | 89 ---- .specify/scripts/bash/common.sh | 121 ----- .specify/scripts/bash/create-new-feature.sh | 135 ----- .specify/scripts/bash/setup-plan.sh | 44 -- .specify/scripts/bash/setup-tasks.sh | 45 -- .specify/templates/constitution-template.md | 54 -- .specify/templates/plan-template.md | 93 ---- .specify/templates/spec-template.md | 89 ---- .specify/templates/tasks-template.md | 67 --- CONTRIBUTING.md | 40 -- LICENSE | 65 ++- README.md | 473 +++--------------- SECURITY.md | 20 - actions/api-client-publish/run.sh | 7 + actions/deploy-bundle/action.yml | 68 +++ actions/deploy-bundle/run.sh | 170 +++++++ actions/deploy-config-render-drift/run.sh | 1 - actions/deploy-sources-render/action.yml | 87 ++++ actions/deploy-sources-render/run.sh | 183 +++++++ actions/platform-config-validate/run.sh | 1 - actions/setup-node/action.yml | 2 +- specs/001-round2-reusable-workflows/plan.md | 43 -- specs/001-round2-reusable-workflows/spec.md | 60 --- specs/001-round2-reusable-workflows/tasks.md | 9 - .../plan.md | 56 --- .../spec.md | 65 --- .../tasks.md | 8 - .../plan.md | 56 --- .../spec.md | 81 --- .../tasks.md | 14 - tests/test_crac_train_workflow.py | 33 +- 42 files changed, 1070 insertions(+), 1687 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/add-to-project.yml create mode 100644 .github/workflows/deploy-bundle.yml create mode 100644 .github/workflows/deploy-sources-render.yml create mode 100644 .github/workflows/repository-hygiene-guard.yml delete mode 100644 .specify/.agent-kit-sdd-seed.sha256 delete mode 100644 .specify/memory/constitution.md delete mode 100755 .specify/scripts/bash/check-prerequisites.sh delete mode 100755 .specify/scripts/bash/common.sh delete mode 100755 .specify/scripts/bash/create-new-feature.sh delete mode 100755 .specify/scripts/bash/setup-plan.sh delete mode 100755 .specify/scripts/bash/setup-tasks.sh delete mode 100644 .specify/templates/constitution-template.md delete mode 100644 .specify/templates/plan-template.md delete mode 100644 .specify/templates/spec-template.md delete mode 100644 .specify/templates/tasks-template.md delete mode 100644 CONTRIBUTING.md delete mode 100644 SECURITY.md create mode 100644 actions/deploy-bundle/action.yml create mode 100755 actions/deploy-bundle/run.sh create mode 100644 actions/deploy-sources-render/action.yml create mode 100755 actions/deploy-sources-render/run.sh delete mode 100644 specs/001-round2-reusable-workflows/plan.md delete mode 100644 specs/001-round2-reusable-workflows/spec.md delete mode 100644 specs/001-round2-reusable-workflows/tasks.md delete mode 100644 specs/002-round3-crac-sidecars-compose-stack/plan.md delete mode 100644 specs/002-round3-crac-sidecars-compose-stack/spec.md delete mode 100644 specs/002-round3-crac-sidecars-compose-stack/tasks.md delete mode 100644 specs/003-round4-compose-system-test-stack/plan.md delete mode 100644 specs/003-round4-compose-system-test-stack/spec.md delete mode 100644 specs/003-round4-compose-system-test-stack/tasks.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..38d9fcb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +* text=auto eol=lf + +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.md text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 83cfb67..5efed9f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Default owner for everything in this repo. -* @JorisJonkers-dev +* @JorisJonkers diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml new file mode 100644 index 0000000..3be0ecc --- /dev/null +++ b/.github/workflows/add-to-project.yml @@ -0,0 +1,61 @@ +name: 'Add to Project' + +'on': + 'workflow_call': + 'inputs': + 'project-url': + 'description': 'Organization Project URL.' + 'required': false + 'type': 'string' + 'default': 'https://github.com/orgs/JorisJonkers-dev/projects/2' + 'secrets': + 'PROJECT_AUTOMATION_APP_ID': + 'description': 'GitHub App id with organization Projects write permission.' + 'required': false + 'PROJECT_AUTOMATION_APP_PRIVATE_KEY': + 'description': 'Private key for the organization Project automation App.' + 'required': false + 'PROJECT_AUTOMATION_TOKEN': + 'description': 'Fallback token with organization Project write permission.' + 'required': false + +'permissions': + 'contents': 'read' + +'jobs': + 'add-to-project': + 'name': 'Add to Project' + 'runs-on': 'ubuntu-latest' + 'if': '${{ github.event.issue.node_id != '''' || github.event.pull_request.node_id != '''' }}' + 'env': + 'PROJECT_AUTOMATION_APP_ID': '${{ secrets.PROJECT_AUTOMATION_APP_ID }}' + 'PROJECT_AUTOMATION_APP_PRIVATE_KEY': '${{ secrets.PROJECT_AUTOMATION_APP_PRIVATE_KEY }}' + 'steps': + - 'name': 'Mint organization Project token' + 'id': 'app-token' + 'if': '${{ env.PROJECT_AUTOMATION_APP_ID != '''' && env.PROJECT_AUTOMATION_APP_PRIVATE_KEY != '''' }}' + 'uses': 'actions/create-github-app-token@v3' + 'with': + 'app-id': '${{ env.PROJECT_AUTOMATION_APP_ID }}' + 'private-key': '${{ env.PROJECT_AUTOMATION_APP_PRIVATE_KEY }}' + 'owner': '${{ github.repository_owner }}' + 'permission-organization-projects': 'write' + 'permission-issues': 'read' + 'permission-pull-requests': 'read' + + - 'name': 'Validate Project token' + 'env': + 'PROJECT_TOKEN': '${{ steps.app-token.outputs.token || secrets.PROJECT_AUTOMATION_TOKEN }}' + 'run': | + set -euo pipefail + + if [ -z "$PROJECT_TOKEN" ]; then + echo '::error::Configure PROJECT_AUTOMATION_APP_ID and PROJECT_AUTOMATION_APP_PRIVATE_KEY, or PROJECT_AUTOMATION_TOKEN.' + exit 1 + fi + + - 'name': 'Add issue or pull request' + 'uses': 'actions/add-to-project@v2.0.0' + 'with': + 'project-url': '${{ inputs.project-url }}' + 'github-token': '${{ steps.app-token.outputs.token || secrets.PROJECT_AUTOMATION_TOKEN }}' diff --git a/.github/workflows/container-publish.yml b/.github/workflows/container-publish.yml index b7be53c..9780665 100644 --- a/.github/workflows/container-publish.yml +++ b/.github/workflows/container-publish.yml @@ -47,7 +47,7 @@ jobs: - uses: docker/setup-buildx-action@v4 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/deploy-bundle.yml b/.github/workflows/deploy-bundle.yml new file mode 100644 index 0000000..32f327d --- /dev/null +++ b/.github/workflows/deploy-bundle.yml @@ -0,0 +1,121 @@ +name: 'Deploy Bundle' + +'on': + 'workflow_call': + 'inputs': + 'deploy-dir': + 'description': 'Path to the caller-owned deploy directory.' + 'required': false + 'type': 'string' + 'default': 'deploy' + 'images': + 'description': 'Newline- or comma-separated image refs to include in the bundle manifest.' + 'required': false + 'type': 'string' + 'default': '' + 'package-version': + 'description': 'Version range for @jorisjonkers-dev/deploy-config-schema.' + 'required': false + 'type': 'string' + 'default': '^0.6.0' + 'version': + 'description': 'Bundle version. Defaults to the ref name or a sha-prefixed development version.' + 'required': false + 'type': 'string' + 'default': '' + 'bundle-name': + 'description': 'Bundle package basename. Defaults to the repository name.' + 'required': false + 'type': 'string' + 'default': '' + 'publish': + 'description': 'Publish the packed deploy bundle to GHCR.' + 'required': false + 'type': 'boolean' + 'default': false + 'secrets': + 'packages-token': + 'description': 'Token used for GitHub Packages reads and GHCR writes.' + 'required': false + 'outputs': + 'bundle-ref': + 'description': 'Published or publishable OCI bundle ref.' + 'value': '${{ jobs.deploy-bundle.outputs.bundle-ref }}' + 'bundle-path': + 'description': 'Packed bundle tarball path.' + 'value': '${{ jobs.deploy-bundle.outputs.bundle-path }}' + +'permissions': + 'contents': 'read' + 'packages': 'write' + +'jobs': + 'deploy-bundle': + 'name': 'Deploy Bundle' + 'runs-on': 'ubuntu-latest' + 'outputs': + 'bundle-ref': '${{ steps.bundle-ref.outputs.bundle-ref }}' + 'bundle-path': '${{ steps.bundle.outputs.bundle-path }}' + 'steps': + - 'uses': 'actions/checkout@v7' + + - 'uses': 'actions/checkout@v7' + 'with': + 'repository': 'JorisJonkers-dev/github-workflows' + 'ref': '${{ github.job_workflow_sha }}' + 'path': '.github-workflows' + 'persist-credentials': false + + - 'name': 'Pack deploy bundle' + 'id': 'bundle' + 'uses': './.github-workflows/actions/deploy-bundle' + 'with': + 'deploy-dir': '${{ inputs.deploy-dir }}' + 'images': '${{ inputs.images }}' + 'package-version': '${{ inputs.package-version }}' + 'version': '${{ inputs.version }}' + 'bundle-name': '${{ inputs.bundle-name }}' + 'node-auth-token': '${{ secrets.packages-token || github.token }}' + + - 'name': 'Resolve bundle ref' + 'id': 'bundle-ref' + 'env': + 'BUNDLE_NAME': '${{ steps.bundle.outputs.bundle-name }}' + 'BUNDLE_VERSION': '${{ steps.bundle.outputs.bundle-version }}' + 'GITHUB_REPOSITORY_OWNER': '${{ github.repository_owner }}' + 'run': | + set -euo pipefail + + owner="$(printf '%s' "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" + name="$(printf '%s' "$BUNDLE_NAME" | tr '[:upper:]' '[:lower:]')" + printf 'bundle-ref=ghcr.io/%s/%s-deploy-bundle:%s\n' "$owner" "$name" "$BUNDLE_VERSION" >> "$GITHUB_OUTPUT" + + - 'name': 'Upload deploy bundle artifact' + 'uses': 'actions/upload-artifact@v5' + 'with': + 'name': 'deploy-bundle' + 'path': '${{ steps.bundle.outputs.bundle-path }}' + 'if-no-files-found': 'error' + 'retention-days': 7 + + - 'uses': 'oras-project/setup-oras@v2' + 'if': '${{ inputs.publish }}' + + - 'name': 'Log in to GHCR' + 'if': '${{ inputs.publish }}' + 'env': + 'GHCR_TOKEN': '${{ secrets.packages-token || github.token }}' + 'run': | + set -euo pipefail + + oras login ghcr.io --username "$GITHUB_ACTOR" --password-stdin <<< "$GHCR_TOKEN" + + - 'name': 'Publish deploy bundle' + 'if': '${{ inputs.publish }}' + 'env': + 'BUNDLE_PATH': '${{ steps.bundle.outputs.bundle-path }}' + 'BUNDLE_REF': '${{ steps.bundle-ref.outputs.bundle-ref }}' + 'run': | + set -euo pipefail + + oras push "$BUNDLE_REF" "$BUNDLE_PATH:application/vnd.jorisjonkers.deployment.bundle.v1+tar" diff --git a/.github/workflows/deploy-sources-render.yml b/.github/workflows/deploy-sources-render.yml new file mode 100644 index 0000000..8e3f855 --- /dev/null +++ b/.github/workflows/deploy-sources-render.yml @@ -0,0 +1,104 @@ +name: 'Deploy Sources Render' + +'on': + 'workflow_call': + 'inputs': + 'package-version': + 'description': 'Version range for @jorisjonkers-dev/deploy-config-schema.' + 'required': false + 'type': 'string' + 'default': '^0.6.0' + 'environment': + 'description': 'Deployment environment to compile.' + 'required': false + 'type': 'string' + 'default': 'production' + 'sources-path': + 'description': 'Path to deployment-sources.yml.' + 'required': false + 'type': 'string' + 'default': 'deployment-sources.yml' + 'lock-path': + 'description': 'Path to deployment.lock.yml.' + 'required': false + 'type': 'string' + 'default': 'deployment.lock.yml' + 'node-contract-path': + 'description': 'Path to the locked node contract.' + 'required': false + 'type': 'string' + 'default': 'inventory/node-contract.lock.yml' + 'reachability-path': + 'description': 'Path to reachability catalog.' + 'required': false + 'type': 'string' + 'default': 'catalog/reachability.yml' + 'output-path': + 'description': 'Rendered Flux output directory.' + 'required': false + 'type': 'string' + 'default': 'cluster/flux' + 'update-lock': + 'description': 'Run lock --update before compile.' + 'required': false + 'type': 'boolean' + 'default': false + 'check': + 'description': 'Require compiled output and derived lock files to be committed.' + 'required': false + 'type': 'boolean' + 'default': true + 'cutover-parity': + 'description': 'Run cutover parity against current-tree.' + 'required': false + 'type': 'boolean' + 'default': false + 'current-tree': + 'description': 'Current live Flux tree used when cutover-parity is true.' + 'required': false + 'type': 'string' + 'default': '' + 'secrets': + 'packages-token': + 'description': 'Token used for GitHub Packages reads.' + 'required': false + 'outputs': + 'image-tags': + 'description': 'Image tags resolved from deployment.lock.yml.' + 'value': '${{ jobs.deploy-sources-render.outputs.image-tags }}' + +'permissions': + 'contents': 'read' + +'jobs': + 'deploy-sources-render': + 'name': 'Deploy Sources Render' + 'runs-on': 'ubuntu-latest' + 'outputs': + 'image-tags': '${{ steps.render.outputs.image-tags }}' + 'steps': + - 'uses': 'actions/checkout@v7' + + - 'uses': 'actions/checkout@v7' + 'with': + 'repository': 'JorisJonkers-dev/github-workflows' + 'ref': '${{ github.job_workflow_sha }}' + 'path': '.github-workflows' + 'persist-credentials': false + + - 'name': 'Render deployment sources' + 'id': 'render' + 'uses': './.github-workflows/actions/deploy-sources-render' + 'with': + 'package-version': '${{ inputs.package-version }}' + 'environment': '${{ inputs.environment }}' + 'sources-path': '${{ inputs.sources-path }}' + 'lock-path': '${{ inputs.lock-path }}' + 'node-contract-path': '${{ inputs.node-contract-path }}' + 'reachability-path': '${{ inputs.reachability-path }}' + 'output-path': '${{ inputs.output-path }}' + 'update-lock': '${{ inputs.update-lock }}' + 'check': '${{ inputs.check }}' + 'cutover-parity': '${{ inputs.cutover-parity }}' + 'current-tree': '${{ inputs.current-tree }}' + 'node-auth-token': '${{ secrets.packages-token || github.token }}' diff --git a/.github/workflows/publish-api-clients.yml b/.github/workflows/publish-api-clients.yml index 97d7c7e..3a5a724 100644 --- a/.github/workflows/publish-api-clients.yml +++ b/.github/workflows/publish-api-clients.yml @@ -76,7 +76,7 @@ jobs: # Install Node + configure the GitHub Packages registry only. Do NOT install here — # the typescript project is created by api-client-publish, which runs npm install itself. - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} registry-url: ${{ inputs.npm-registry-url }} diff --git a/.github/workflows/repository-hygiene-guard.yml b/.github/workflows/repository-hygiene-guard.yml new file mode 100644 index 0000000..2bdff96 --- /dev/null +++ b/.github/workflows/repository-hygiene-guard.yml @@ -0,0 +1,104 @@ +name: 'Repository Hygiene Guard' + +'on': + 'workflow_call': + 'inputs': + 'base-ref': + 'description': 'Base ref used for PR comparison.' + 'required': false + 'type': 'string' + 'default': '' + 'override-label': + 'description': 'Pull request label that bypasses this guard.' + 'required': false + 'type': 'string' + 'default': 'allow-repository-hygiene-exception' + 'allowlist-regex': + 'description': 'Python regex for intentional exceptions.' + 'required': false + 'type': 'string' + 'default': '' + 'deny-globs': + 'description': 'Newline-separated denied path globs.' + 'required': false + 'type': 'string' + 'default': | + .specify/** + specs/[0-9][0-9][0-9]-*/** + plans/** + planning/** + scratchpad/** + scratchpads/** + .agent-council/** + **/*scratchpad*.md + **/*.scratch.md + **/*agent-council*.md + .claude/commands/speckit*.md + .claude/skills/speckit*/** + .agents/skills/speckit*/** + templates/**/.specify/** + templates/**/.claude/commands/speckit*.md + +'permissions': + 'contents': 'read' + 'pull-requests': 'read' + +'jobs': + 'guard': + 'name': 'Repository Hygiene Guard' + 'runs-on': 'ubuntu-latest' + 'if': '${{ !contains(github.event.pull_request.labels.*.name, inputs.override-label) }}' + 'steps': + - 'uses': 'actions/checkout@v7' + 'with': + 'fetch-depth': 0 + + - 'name': 'Check denied planning artifacts' + 'env': + 'BASE_REF_INPUT': '${{ inputs.base-ref }}' + 'BASE_REF_FALLBACK': '${{ format(''origin/{0}'', github.event.pull_request.base.ref || github.event.repository.default_branch || ''main'') }}' + 'DENY_GLOBS': '${{ inputs.deny-globs }}' + 'ALLOWLIST_REGEX': '${{ inputs.allowlist-regex }}' + 'run': | + set -euo pipefail + + base_ref="${BASE_REF_INPUT:-$BASE_REF_FALLBACK}" + git fetch --no-tags --depth=1 origin "${base_ref#origin/}" || true + export BASE_REF="$base_ref" + + python3 - <<'PY' + import fnmatch + import os + import re + import subprocess + import sys + + base_ref = os.environ['BASE_REF'] + deny_globs = [ + line.strip() + for line in os.environ['DENY_GLOBS'].splitlines() + if line.strip() and not line.strip().startswith('#') + ] + allowlist_raw = os.environ.get('ALLOWLIST_REGEX', '').strip() + allowlist = re.compile(allowlist_raw) if allowlist_raw else None + + changed = subprocess.check_output( + ['git', 'diff', '--name-only', '--diff-filter=ACMR', f'{base_ref}...HEAD'], + text=True, + ).splitlines() + + denied = [] + for path in changed: + if allowlist and allowlist.search(path): + continue + if any(fnmatch.fnmatchcase(path, pattern) for pattern in deny_globs): + denied.append(path) + + if denied: + print('::error::Denied planning/agent scratch artifacts were added or modified:') + for path in denied: + print(f' - {path}') + print('') + print('Move planning material to issues/PRs or durable docs, or apply the override label for an approved exception.') + sys.exit(1) + PY diff --git a/.gitignore b/.gitignore index aacad0b..9489512 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,15 @@ node_modules/ # Logs *.log + +# Planning and agent scratch artifacts belong in issues, PRs, or durable docs. +.specify/ +specs/[0-9][0-9][0-9]-*/ +plans/ +planning/ +scratchpad/ +scratchpads/ +.agent-council/ +*scratchpad*.md +*.scratch.md +*agent-council*.md diff --git a/.specify/.agent-kit-sdd-seed.sha256 b/.specify/.agent-kit-sdd-seed.sha256 deleted file mode 100644 index 345173b..0000000 --- a/.specify/.agent-kit-sdd-seed.sha256 +++ /dev/null @@ -1,10 +0,0 @@ -3ba571da05c7f0ae9d13ae44bd636f41f029e7e1598376850702954916b51551 templates/constitution-template.md -04026ce6621c81392ce8ea7f59f9374087c8798b2d95f31094c18bab3cd0fc52 templates/plan-template.md -f466d7ee0e5870c5d8a54408293a3c41d04a55a90049f5b7c42feefc4e9b2454 templates/spec-template.md -ffa552b4937a104926c8ed2f09ae3b09dd9e78aebf1b5770b3bbb4ee2bbaad77 templates/tasks-template.md -4e477664b02ccfc45dd1d795ece01006a3c59ff0088a5dce985ccd0a82bddd70 scripts/bash/check-prerequisites.sh -f4ccdd5e3adde5b9dc25375d6da8fbb2d15da704652b4d8cd6e81b29b3ab535f scripts/bash/common.sh -b9ec52760baf03c4077f22d30effddf97b4a2a18927b1c024a440579556ddda9 scripts/bash/create-new-feature.sh -4d3f657e673ad4192a4c12af86491cf131e29ddffcf00729c0a28b3ee421fb66 scripts/bash/setup-plan.sh -23004d89fe3ede0ef062ccb287eeb2d34e90fbcc4ab16912e517cf43d528039a scripts/bash/setup-tasks.sh -3ba571da05c7f0ae9d13ae44bd636f41f029e7e1598376850702954916b51551 memory/constitution.md diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md deleted file mode 100644 index fd2bfde..0000000 --- a/.specify/memory/constitution.md +++ /dev/null @@ -1,54 +0,0 @@ -# Project Constitution - -## Core Principles - -### I. Outcome-First Specifications - -Every feature begins with a specification that describes user-visible outcomes, -acceptance scenarios, non-goals, and success criteria before implementation -details. Ambiguity must be marked explicitly with `NEEDS CLARIFICATION`. - -### II. Plan Before Implementation - -Implementation work starts only after the plan identifies real project paths, -dependencies, validation commands, rollback considerations, and risks. Plans -must prefer established local patterns over new abstractions. - -### III. Tests and Validation Are Mandatory - -Each feature defines the smallest meaningful verification command before work -begins. Changes are not complete until those checks pass or the remaining gap is -documented with the exact reason validation could not run. - -### IV. Small, Reviewable Changes - -Tasks and PRs must be independently reviewable, revertable, and scoped to one -behavioral objective. Unrelated cleanup, broad refactors, and speculative -flexibility are not allowed inside feature work. - -### V. Durable Context Stays Current - -Specifications, plans, tasks, and durable project memory must reflect decisions -that affect future work. Do not leave important behavior only in chat logs, -temporary notes, or uncommitted local state. - -## Workflow - -1. `/speckit.specify` creates or updates `specs//spec.md`. -2. `/speckit.plan` creates `plan.md` and supporting design artifacts. -3. `/speckit.tasks` creates `tasks.md` from the approved plan. -4. Implementation follows tasks in dependency order, with tests close to the - behavior being changed. -5. Completion requires validation evidence and any relevant documentation - updates. - -## Governance - -This constitution overrides informal conventions. Changes to these principles -must be reviewed deliberately, with downstream templates and instructions -updated in the same change. - -**Version**: 1.0.0 -**Ratified**: {{DATE}} -**Last Amended**: {{DATE}} - diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh deleted file mode 100755 index 3572ca0..0000000 --- a/.specify/scripts/bash/check-prerequisites.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SPECIFY_SCRIPT_DIR=$(unset CDPATH; cd -- "$(dirname -- "$0")" && pwd -P) -# shellcheck disable=SC1091 -. "${SPECIFY_SCRIPT_DIR}/common.sh" - -json=false -paths_only=false -require_tasks=false -include_tasks=false - -while [ "$#" -gt 0 ]; do - case "$1" in - --json) - json=true - ;; - --paths-only) - paths_only=true - ;; - --require-tasks) - require_tasks=true - ;; - --include-tasks) - include_tasks=true - ;; - --help|-h) - printf 'Usage: %s [--json] [--paths-only] [--require-tasks] [--include-tasks]\n' "$(basename "$0")" - exit 0 - ;; - --*) - specify_die "Unknown option: $1" - ;; - esac - shift -done - -branch=$(specify_require_feature_branch) -feature_dir=$(specify_feature_dir "${branch}") -spec_file=$(specify_spec_file "${branch}") -plan_file=$(specify_plan_file "${branch}") -tasks_file=$(specify_tasks_file "${branch}") - -if [ "${paths_only}" = true ]; then - if [ "${json}" = true ]; then - specify_paths_json "${branch}" - else - printf 'REPO_ROOT: %s\n' "${REPO_ROOT}" - printf 'BRANCH: %s\n' "${branch}" - printf 'FEATURE_DIR: %s\n' "${feature_dir}" - printf 'FEATURE_SPEC: %s\n' "${spec_file}" - printf 'IMPL_PLAN: %s\n' "${plan_file}" - printf 'TASKS: %s\n' "${tasks_file}" - fi - exit 0 -fi - -[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" -[ -f "${spec_file}" ] || specify_die "Feature spec not found: ${spec_file}" -[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" - -if [ "${require_tasks}" = true ] && [ ! -f "${tasks_file}" ]; then - specify_die "Tasks file not found: ${tasks_file}" -fi - -docs="" -append_doc() { - if [ -n "${docs}" ]; then - docs="${docs}," - fi - docs="${docs}\"$1\"" -} - -[ -f "${feature_dir}/research.md" ] && append_doc "research.md" -[ -f "${feature_dir}/data-model.md" ] && append_doc "data-model.md" -[ -d "${feature_dir}/contracts" ] && append_doc "contracts/" -[ -f "${feature_dir}/quickstart.md" ] && append_doc "quickstart.md" -if [ "${include_tasks}" = true ] && [ -f "${tasks_file}" ]; then - append_doc "tasks.md" -fi - -if [ "${json}" = true ]; then - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":[%s]}\n' "$(specify_json_escape "${feature_dir}")" "${docs}" -else - printf 'FEATURE_DIR: %s\n' "${feature_dir}" - printf 'AVAILABLE_DOCS:\n' - printf '%s\n' "${docs}" | tr ',' '\n' | sed 's/^/ /; s/"//g' -fi diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh deleted file mode 100755 index 9dc5987..0000000 --- a/.specify/scripts/bash/common.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [ -z "${SPECIFY_SCRIPT_DIR:-}" ]; then - SPECIFY_SCRIPT_DIR=$(unset CDPATH; cd -- "$(dirname -- "$0")" && pwd -P) -fi - -specify_repo_root() { - if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then - git rev-parse --show-toplevel - return 0 - fi - - (unset CDPATH; cd -- "${SPECIFY_SCRIPT_DIR}/../../.." && pwd -P) -} - -REPO_ROOT=$(specify_repo_root) -SPECIFY_DIR="${REPO_ROOT}/.specify" -# shellcheck disable=SC2034 -SPECS_DIR="${REPO_ROOT}/specs" - -specify_has_git() { - command -v git >/dev/null 2>&1 && git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1 -} - -specify_current_branch() { - if specify_has_git; then - git -C "${REPO_ROOT}" rev-parse --abbrev-ref HEAD 2>/dev/null - else - basename "$(pwd)" - fi -} - -specify_is_feature_branch() { - case "$1" in - [0-9][0-9][0-9]-*) return 0 ;; - *) return 1 ;; - esac -} - -specify_feature_dir() { - printf '%s/specs/%s\n' "${REPO_ROOT}" "$1" -} - -specify_spec_file() { - printf '%s/specs/%s/spec.md\n' "${REPO_ROOT}" "$1" -} - -specify_plan_file() { - printf '%s/specs/%s/plan.md\n' "${REPO_ROOT}" "$1" -} - -specify_tasks_file() { - printf '%s/specs/%s/tasks.md\n' "${REPO_ROOT}" "$1" -} - -specify_template_file() { - printf '%s/templates/%s\n' "${SPECIFY_DIR}" "$1" -} - -specify_json_escape() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' -} - -specify_error() { - printf 'ERROR: %s\n' "$*" >&2 -} - -specify_die() { - specify_error "$*" - exit 1 -} - -specify_require_feature_branch() { - current_branch=$(specify_current_branch) - if [ "${current_branch}" = "main" ] || [ "${current_branch}" = "master" ]; then - specify_die "Current branch '${current_branch}' is not a feature branch. Run create-new-feature.sh first." - fi - if ! specify_is_feature_branch "${current_branch}"; then - specify_die "Current branch '${current_branch}' must start with a three-digit feature number, e.g. 001-my-feature." - fi - printf '%s\n' "${current_branch}" -} - -specify_copy_template() { - template_name=$1 - destination=$2 - feature_name=$3 - source_file=$(specify_template_file "${template_name}") - - [ -f "${source_file}" ] || specify_die "Template not found: ${source_file}" - - today=$(date +%F) - sed \ - -e "s/{{FEATURE_NAME}}/${feature_name}/g" \ - -e "s/{{feature_name}}/${feature_name}/g" \ - -e "s/{{DATE}}/${today}/g" \ - "${source_file}" > "${destination}" -} - -specify_paths_json() { - branch=$1 - feature_dir=$(specify_feature_dir "${branch}") - spec_file=$(specify_spec_file "${branch}") - plan_file=$(specify_plan_file "${branch}") - tasks_file=$(specify_tasks_file "${branch}") - has_git=false - if specify_has_git; then - has_git=true - fi - - printf '{"REPO_ROOT":"%s","BRANCH":"%s","HAS_GIT":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ - "$(specify_json_escape "${REPO_ROOT}")" \ - "$(specify_json_escape "${branch}")" \ - "${has_git}" \ - "$(specify_json_escape "${feature_dir}")" \ - "$(specify_json_escape "${spec_file}")" \ - "$(specify_json_escape "${plan_file}")" \ - "$(specify_json_escape "${tasks_file}")" -} diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh deleted file mode 100755 index 21dcee7..0000000 --- a/.specify/scripts/bash/create-new-feature.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SPECIFY_SCRIPT_DIR=$(unset CDPATH; cd -- "$(dirname -- "$0")" && pwd -P) -# shellcheck disable=SC1091 -. "${SPECIFY_SCRIPT_DIR}/common.sh" - -json=false -feature_number="" -description="" - -while [ "$#" -gt 0 ]; do - case "$1" in - --json) - json=true - ;; - --number) - shift - [ "$#" -gt 0 ] || specify_die "--number requires a value" - feature_number=$1 - ;; - --help|-h) - printf 'Usage: %s [--json] [--number N] \n' "$(basename "$0")" - exit 0 - ;; - --*) - specify_die "Unknown option: $1" - ;; - *) - if [ -z "${description}" ]; then - description=$1 - else - description="${description} $1" - fi - ;; - esac - shift -done - -[ -n "${description}" ] || specify_die "Feature description is required" - -slug=$(printf '%s\n' "${description}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g' | awk ' -{ - count = 0 - for (i = 1; i <= NF; i++) { - word = $i - if (word == "the" || word == "and" || word == "for" || word == "with" || word == "from" || word == "that" || word == "this" || word == "into" || word == "onto") { - continue - } - if (length(word) < 2) { - continue - } - words[++count] = word - if (count == 5) { - break - } - } - if (count == 0) { - print "feature" - } else { - for (i = 1; i <= count; i++) { - printf "%s%s", (i == 1 ? "" : "-"), words[i] - } - printf "\n" - } -}') - -highest=0 -if specify_has_git; then - branches=$(git -C "${REPO_ROOT}" branch --all --format='%(refname:short)' 2>/dev/null || true) - for ref in ${branches}; do - ref=${ref#origin/} - number=${ref%%-*} - case "${number}" in - [0-9][0-9][0-9]) - if [ "${number}" -gt "${highest}" ]; then - highest=${number} - fi - ;; - esac - done -fi - -if [ -d "${SPECS_DIR}" ]; then - for path in "${SPECS_DIR}"/[0-9][0-9][0-9]-*; do - [ -d "${path}" ] || continue - name=$(basename "${path}") - number=${name%%-*} - case "${number}" in - [0-9][0-9][0-9]) - if [ "${number}" -gt "${highest}" ]; then - highest=${number} - fi - ;; - esac - done -fi - -if [ -n "${feature_number}" ]; then - case "${feature_number}" in - *[!0-9]*) specify_die "--number must be numeric" ;; - esac - number=$(printf '%03d' "${feature_number}") -else - number=$(printf '%03d' $((highest + 1))) -fi - -branch_name="${number}-${slug}" -feature_dir=$(specify_feature_dir "${branch_name}") -spec_file=$(specify_spec_file "${branch_name}") - -if specify_has_git; then - if ! git -C "${REPO_ROOT}" rev-parse --verify --quiet "${branch_name}" >/dev/null; then - git -C "${REPO_ROOT}" checkout -b "${branch_name}" >/dev/null 2>&1 - else - git -C "${REPO_ROOT}" checkout "${branch_name}" >/dev/null 2>&1 - fi -fi - -mkdir -p "${feature_dir}" -if [ ! -f "${spec_file}" ]; then - specify_copy_template "spec-template.md" "${spec_file}" "${branch_name}" -fi - -if [ "${json}" = true ]; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_DIR":"%s","FEATURE_NUMBER":"%s"}\n' \ - "$(specify_json_escape "${branch_name}")" \ - "$(specify_json_escape "${spec_file}")" \ - "$(specify_json_escape "${feature_dir}")" \ - "$(specify_json_escape "${number}")" -else - printf 'BRANCH_NAME: %s\n' "${branch_name}" - printf 'SPEC_FILE: %s\n' "${spec_file}" -fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh deleted file mode 100755 index 29d72fb..0000000 --- a/.specify/scripts/bash/setup-plan.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SPECIFY_SCRIPT_DIR=$(unset CDPATH; cd -- "$(dirname -- "$0")" && pwd -P) -# shellcheck disable=SC1091 -. "${SPECIFY_SCRIPT_DIR}/common.sh" - -json=false -while [ "$#" -gt 0 ]; do - case "$1" in - --json) - json=true - ;; - --help|-h) - printf 'Usage: %s [--json]\n' "$(basename "$0")" - exit 0 - ;; - --*) - specify_die "Unknown option: $1" - ;; - esac - shift -done - -branch=$(specify_require_feature_branch) -feature_dir=$(specify_feature_dir "${branch}") -spec_file=$(specify_spec_file "${branch}") -plan_file=$(specify_plan_file "${branch}") - -mkdir -p "${feature_dir}" "${feature_dir}/contracts" -[ -f "${spec_file}" ] || specify_copy_template "spec-template.md" "${spec_file}" "${branch}" -[ -f "${plan_file}" ] || specify_copy_template "plan-template.md" "${plan_file}" "${branch}" - -touch "${feature_dir}/research.md" "${feature_dir}/data-model.md" "${feature_dir}/quickstart.md" - -if [ "${json}" = true ]; then - specify_paths_json "${branch}" -else - printf 'FEATURE_SPEC: %s\n' "${spec_file}" - printf 'IMPL_PLAN: %s\n' "${plan_file}" - printf 'SPECS_DIR: %s\n' "${feature_dir}" - printf 'BRANCH: %s\n' "${branch}" -fi diff --git a/.specify/scripts/bash/setup-tasks.sh b/.specify/scripts/bash/setup-tasks.sh deleted file mode 100755 index 1452fcb..0000000 --- a/.specify/scripts/bash/setup-tasks.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SPECIFY_SCRIPT_DIR=$(unset CDPATH; cd -- "$(dirname -- "$0")" && pwd -P) -# shellcheck disable=SC1091 -. "${SPECIFY_SCRIPT_DIR}/common.sh" - -json=false -while [ "$#" -gt 0 ]; do - case "$1" in - --json) - json=true - ;; - --help|-h) - printf 'Usage: %s [--json]\n' "$(basename "$0")" - exit 0 - ;; - --*) - specify_die "Unknown option: $1" - ;; - esac - shift -done - -branch=$(specify_require_feature_branch) -feature_dir=$(specify_feature_dir "${branch}") -plan_file=$(specify_plan_file "${branch}") -tasks_file=$(specify_tasks_file "${branch}") - -[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" -[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" - -if [ ! -f "${tasks_file}" ]; then - specify_copy_template "tasks-template.md" "${tasks_file}" "${branch}" -fi - -if [ "${json}" = true ]; then - specify_paths_json "${branch}" -else - printf 'TASKS: %s\n' "${tasks_file}" - printf 'IMPL_PLAN: %s\n' "${plan_file}" - printf 'SPECS_DIR: %s\n' "${feature_dir}" - printf 'BRANCH: %s\n' "${branch}" -fi diff --git a/.specify/templates/constitution-template.md b/.specify/templates/constitution-template.md deleted file mode 100644 index fd2bfde..0000000 --- a/.specify/templates/constitution-template.md +++ /dev/null @@ -1,54 +0,0 @@ -# Project Constitution - -## Core Principles - -### I. Outcome-First Specifications - -Every feature begins with a specification that describes user-visible outcomes, -acceptance scenarios, non-goals, and success criteria before implementation -details. Ambiguity must be marked explicitly with `NEEDS CLARIFICATION`. - -### II. Plan Before Implementation - -Implementation work starts only after the plan identifies real project paths, -dependencies, validation commands, rollback considerations, and risks. Plans -must prefer established local patterns over new abstractions. - -### III. Tests and Validation Are Mandatory - -Each feature defines the smallest meaningful verification command before work -begins. Changes are not complete until those checks pass or the remaining gap is -documented with the exact reason validation could not run. - -### IV. Small, Reviewable Changes - -Tasks and PRs must be independently reviewable, revertable, and scoped to one -behavioral objective. Unrelated cleanup, broad refactors, and speculative -flexibility are not allowed inside feature work. - -### V. Durable Context Stays Current - -Specifications, plans, tasks, and durable project memory must reflect decisions -that affect future work. Do not leave important behavior only in chat logs, -temporary notes, or uncommitted local state. - -## Workflow - -1. `/speckit.specify` creates or updates `specs//spec.md`. -2. `/speckit.plan` creates `plan.md` and supporting design artifacts. -3. `/speckit.tasks` creates `tasks.md` from the approved plan. -4. Implementation follows tasks in dependency order, with tests close to the - behavior being changed. -5. Completion requires validation evidence and any relevant documentation - updates. - -## Governance - -This constitution overrides informal conventions. Changes to these principles -must be reviewed deliberately, with downstream templates and instructions -updated in the same change. - -**Version**: 1.0.0 -**Ratified**: {{DATE}} -**Last Amended**: {{DATE}} - diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md deleted file mode 100644 index 9df3556..0000000 --- a/.specify/templates/plan-template.md +++ /dev/null @@ -1,93 +0,0 @@ -# Implementation Plan: {{FEATURE_NAME}} - -**Branch**: `{{FEATURE_NAME}}` | **Date**: {{DATE}} | **Spec**: [spec.md](./spec.md) -**Input**: Feature specification from `/specs/{{FEATURE_NAME}}/spec.md` - -## Summary - -[Extract from feature spec: primary requirement + technical approach] - -## Technical Context - -**Language/Version**: [e.g. Kotlin 2.x, TypeScript 5.x or NEEDS CLARIFICATION] -**Primary Dependencies**: [e.g. Spring Boot, Vue, Postgres or NEEDS CLARIFICATION] -**Storage**: [if applicable, e.g. PostgreSQL, Redis, files or N/A] -**Testing**: [e.g. Gradle unit tests, Vitest, Playwright or NEEDS CLARIFICATION] -**Target Platform**: [e.g. k3s, browser, JVM service or NEEDS CLARIFICATION] -**Project Type**: [service/ui/platform/mixed] -**Performance Goals**: [domain-specific target or N/A] -**Constraints**: [domain-specific constraints or N/A] -**Scale/Scope**: [domain-specific scale or N/A] - -## Constitution Check - -*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - -- [ ] No attribution is introduced in files, comments, commit text, or PR text -- [ ] Claude/Codex parity is preserved for any agent-facing behavior -- [ ] Rendered artifacts are updated by the owning renderer when source changes require it -- [ ] Small stacked PR boundary is clear and unrelated cleanup is excluded -- [ ] Verification command is identified for each touched area - -## Project Structure - -### Documentation - -```text -specs/{{FEATURE_NAME}}/ -|-- plan.md -|-- research.md -|-- data-model.md -|-- quickstart.md -|-- contracts/ -`-- tasks.md -``` - -### Source Code - -```text -# Fill with the actual paths this feature will touch. -``` - -**Structure Decision**: [Document the chosen source layout and real paths] - -## Phase 0: Outline & Research - -1. Extract unknowns from Technical Context into research tasks. -2. Capture existing repo patterns for touched paths. -3. Resolve all NEEDS CLARIFICATION items before design. - -**Output**: `research.md` - -## Phase 1: Design & Contracts - -1. Derive entities from the feature spec and document them in `data-model.md`. -2. Produce or update API/CLI/config contracts in `contracts/`. -3. Write `quickstart.md` with validation steps for the feature. -4. Re-run Constitution Check. - -**Output**: `data-model.md`, `contracts/*`, `quickstart.md` - -## Phase 2: Task Planning Approach - -Describe how `/speckit.tasks` should convert this plan into ordered, independently executable tasks. Do not create `tasks.md` manually during `/speckit.plan`. - -## Complexity Tracking - -| Violation | Why Needed | Simpler Alternative Rejected Because | -| --- | --- | --- | -| [Only if a constitution gate is intentionally violated] | [reason] | [why simpler option does not work] | - -## Progress Tracking - -**Phase Status**: - -- [ ] Phase 0: Research complete -- [ ] Phase 1: Design complete -- [ ] Phase 2: Task planning approach complete - -**Gate Status**: - -- [ ] Initial Constitution Check: PASS -- [ ] Post-Design Constitution Check: PASS -- [ ] All NEEDS CLARIFICATION resolved diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md deleted file mode 100644 index ba912ca..0000000 --- a/.specify/templates/spec-template.md +++ /dev/null @@ -1,89 +0,0 @@ -# Feature Specification: {{FEATURE_NAME}} - -**Feature Branch**: `{{FEATURE_NAME}}` -**Created**: {{DATE}} -**Status**: Draft -**Input**: User description: "$ARGUMENTS" - -## User Scenarios & Testing *(mandatory)* - - - -### User Story 1 - [Short Title] (Priority: P1) - -[Describe the user journey in plain language] - -**Why this priority**: [Explain the value and why it comes first] - -**Independent Test**: [Describe how to verify this story independently] - -**Acceptance Scenarios**: - -1. **Given** [initial state], **When** [action], **Then** [observable outcome] -2. **Given** [initial state], **When** [action], **Then** [observable outcome] - ---- - -### User Story 2 - [Short Title] (Priority: P2) - -[Describe the user journey in plain language] - -**Why this priority**: [Explain the value] - -**Independent Test**: [Describe how to verify this story independently] - -**Acceptance Scenarios**: - -1. **Given** [initial state], **When** [action], **Then** [observable outcome] - ---- - -### User Story 3 - [Short Title] (Priority: P3) - -[Describe the user journey in plain language] - -**Why this priority**: [Explain the value] - -**Independent Test**: [Describe how to verify this story independently] - -**Acceptance Scenarios**: - -1. **Given** [initial state], **When** [action], **Then** [observable outcome] - -### Edge Cases - -- What happens when [boundary condition]? -- How does the system handle [error condition]? - -## Requirements *(mandatory)* - -### Functional Requirements - -- **FR-001**: System MUST [specific capability] -- **FR-002**: System MUST [specific capability] -- **FR-003**: Users MUST be able to [key interaction] -- **FR-004**: System MUST [data requirement] -- **FR-005**: System MUST [observable behavior] - -*Example of marking unclear requirements:* - -- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified] -- **FR-007**: System MUST retain [NEEDS CLARIFICATION: retention period not specified] - -### Key Entities *(include if feature involves data)* - -- **[Entity 1]**: [What it represents, key attributes without implementation details] -- **[Entity 2]**: [What it represents, relationships to other entities] - -## Success Criteria *(mandatory)* - -### Measurable Outcomes - -- **SC-001**: [Metric, e.g. "Users can complete primary task in under 2 minutes"] -- **SC-002**: [Metric, e.g. "System supports 1,000 concurrent users"] -- **SC-003**: [Metric, e.g. "95% of users complete the task without support"] -- **SC-004**: [Metric, e.g. "Reduce support tickets about X by 50%"] - diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md deleted file mode 100644 index b23ac7f..0000000 --- a/.specify/templates/tasks-template.md +++ /dev/null @@ -1,67 +0,0 @@ -# Tasks: {{FEATURE_NAME}} - -**Input**: Design documents from `/specs/{{FEATURE_NAME}}/` -**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/ - -## Format: `[ID] [P?] [Story] Description` - -- **[P]**: Can run in parallel with other tasks because it touches different files -- **[Story]**: User story label, for example US1, US2, US3 -- Include exact file paths in descriptions - -## Phase 1: Setup - -- [ ] T001 Create or verify project structure for this feature -- [ ] T002 Identify the smallest validation command for touched area - -## Phase 2: Foundational - -- [ ] T003 Implement shared models/configuration needed by all stories -- [ ] T004 Add or update base tests for cross-story behavior - -## Phase 3: User Story 1 (Priority: P1) - -**Goal**: [Brief value delivered by this story] - -**Independent Test**: [How to verify only this story] - -- [ ] T005 [US1] Implement [specific behavior] in [path] -- [ ] T006 [US1] Add focused tests in [path] - -## Phase 4: User Story 2 (Priority: P2) - -**Goal**: [Brief value delivered by this story] - -**Independent Test**: [How to verify only this story] - -- [ ] T007 [P] [US2] Implement [specific behavior] in [path] -- [ ] T008 [P] [US2] Add focused tests in [path] - -## Phase 5: User Story 3 (Priority: P3) - -**Goal**: [Brief value delivered by this story] - -**Independent Test**: [How to verify only this story] - -- [ ] T009 [P] [US3] Implement [specific behavior] in [path] -- [ ] T010 [P] [US3] Add focused tests in [path] - -## Phase 6: Polish - -- [ ] T011 Run the validation command identified in plan.md -- [ ] T012 Update docs or runbooks affected by this feature - -## Dependencies - -- Setup before foundational work -- Foundational work before user stories -- User stories may proceed in priority order, unless marked independent and parallel -- Polish after desired stories are complete - -## Parallel Example - -```text -T007 [P] [US2] ... -T009 [P] [US3] ... -``` - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 4c1678c..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,40 +0,0 @@ -# Contributing (JorisJonkers-dev conventions) - -These conventions are identical across every JorisJonkers-dev repo. This repo is the -template; new repos are bootstrapped from it (see `docs/REPO_SETUP.md`). - -## Branch & PR flow - -- Branch from `main`. Keep PRs small, reviewable, and revertable alone; - stacking (PR B on PR A) is fine. -- Open a PR using the template. **Every PR links its tracking issue / epic.** -- Merge method is **squash only** (enforced by the ruleset), and history is - linear. Use a conventional-commit PR title — release-please derives the next - version from it. -- A PR merges only when the single required check, **`Pipeline Complete`**, is - green. - -## CI: one pipeline, one gate - -Each repo has exactly one CI workflow whose terminal job is named **`Pipeline -Complete`**. It `needs:` every gating job and fails unless all of them -succeeded. The org ruleset requires only that one check, so adding/renaming a -job never touches branch protection — just keep it in the aggregator's -`needs:`. See `.github/workflows/ci.yml`. - -## Versioning - -Exact-pin everything; release via release-please. Full rules in -`VERSIONING.md`. - -## Commit & PR voice - -Impersonal and professional. No `you`/`we`; lead with the observable behaviour -or root cause, then the change. No hedging ("hopefully", "should be fine"); say -what proves it works. Do not add co-author or generated-by trailers. - -## Tracking work - -Work is tracked as issues under a milestone, rolled up to an epic issue. Keep -the issue checklist and status current as PRs land, and reference issues from -PRs (`Closes #`, `Part of #`). diff --git a/LICENSE b/LICENSE index 8b4a705..8bd870f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,16 +1,49 @@ -Copyright (c) 2026 jorisjonkers-dev. All Rights Reserved. - -This software and associated documentation files (the "Software") are the -proprietary and confidential property of the copyright holder. - -No license, express or implied, to use, copy, modify, merge, publish, -distribute, sublicense, or sell copies of the Software is granted except by a -separate written agreement with the copyright holder. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Joris Jonkers Proprietary Source-Available License 1.0 + +Copyright (c) 2026 Joris Jonkers. All rights reserved. + +This repository and its contents, including source code, documentation, +configuration, build scripts, workflows, assets, tests, and generated files, are +the intellectual property of Joris Jonkers unless a file explicitly states +otherwise. + +The source code is made available for viewing only. Publication of this +repository, including publication in a public source-control hosting service, +does not grant any license, permission, or other right to use the repository or +any part of it. + +Except to the extent required by the terms of the source-control hosting service +used to view or fork this repository, or as otherwise required by applicable law, +you may not, without prior written permission from Joris Jonkers: + +1. copy, download, clone, mirror, or archive the repository or any part of it; +2. build, run, execute, deploy, host, or operate the software; +3. modify, adapt, translate, or create derivative works; +4. redistribute, publish, sublicense, sell, rent, lease, or otherwise make the + repository or any part of it available to another person or entity; +5. use the repository or any part of it for commercial, internal business, + production, research, educational, training, benchmarking, or personal + projects; +6. use the repository or any part of it to develop, train, evaluate, or improve + software, models, services, or datasets. + +No patent license, trademark license, database right, trade secret license, or +other intellectual-property right is granted. + +If you have received express written permission from Joris Jonkers, your use is +limited to the scope, duration, and purpose stated in that written permission. +All rights not expressly granted in writing are reserved. + +Third-party dependencies, vendored code, generated artifacts, and external +assets may be subject to their own license terms. Those third-party terms apply +only to the third-party material and do not grant any rights to this repository +or to original work by Joris Jonkers. + +THE REPOSITORY AND ITS CONTENTS ARE PROVIDED FOR VIEWING "AS IS", WITHOUT +WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, +NON-INFRINGEMENT, SECURITY, ACCURACY, OR AVAILABILITY. + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, JORIS JONKERS WILL NOT BE LIABLE FOR +ANY CLAIM, DAMAGE, LOSS, COST, OR EXPENSE ARISING FROM OR RELATED TO THIS +REPOSITORY, ITS CONTENTS, OR ANY UNAUTHORIZED USE OF IT. diff --git a/README.md b/README.md index 443dbb2..f190a14 100644 --- a/README.md +++ b/README.md @@ -1,453 +1,120 @@ # github-workflows -Reusable JorisJonkers-dev composite actions and workflows live here. Consumer -repositories should call them with immutable release tags, not branches. -Renovate keeps those pins current. - -## Composite actions - -### `prepare-ci-host` - -Downloads Docker image artifacts, loads the image tarballs, and optionally runs -a repository-provided dev DNS setup script. - -```yaml -steps: - - uses: actions/checkout@v4 - - uses: JorisJonkers-dev/github-workflows/actions/prepare-ci-host@v0.6.0 - with: - artifact-pattern: image-* - artifact-path: /tmp/images - image-name-pattern: image-* - image-tar-pattern: '*.tar' - configure-dev-dns: 'true' - dev-dns-script-path: infra/scripts/setup-dev-dns.sh -``` - -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `artifact-pattern` | `image-*` | Artifact name pattern passed to `actions/download-artifact`. | -| `artifact-path` | `/tmp/images` | Directory where artifacts are downloaded. | -| `image-name-pattern` | `image-*` | Directory pattern below `artifact-path` that contains tarballs. | -| `image-tar-pattern` | `*.tar` | File pattern for Docker image tarballs. | -| `configure-dev-dns` | `true` | Runs the dev DNS script when set to `true`. | -| `dev-dns-script-path` | `infra/scripts/setup-dev-dns.sh` | Script path in the checked-out repository. | - -### `setup-java-gradle` - -Installs Java and configures Gradle dependency caching. - -```yaml -steps: - - uses: actions/checkout@v4 - - uses: JorisJonkers-dev/github-workflows/actions/setup-java-gradle@v0.6.0 - with: - java-version: '21' - java-distribution: temurin - github-packages-token: ${{ secrets.GITHUB_TOKEN }} -``` - -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `java-version` | `21` | Java version installed by `actions/setup-java`. | -| `java-distribution` | `temurin` | Java distribution installed by `actions/setup-java`. | -| `gradle-cache-disabled` | `false` | Disables Gradle caching when set to `true`. | -| `gradle-cache-read-only` | `false` | Restores Gradle cache entries without saving updates when set to `true`. | -| `github-packages-actor` | `github.actor` | GitHub Packages actor exported as `GITHUB_ACTOR` when `github-packages-token` is set. | -| `github-packages-token` | empty | GitHub Packages token exported as `GITHUB_TOKEN` for Gradle package resolution. | - -### `setup-node` - -Installs Node, configures package-manager caching, and installs dependencies. -The package manager may be npm, pnpm, or Yarn Berry. - -```yaml -steps: - - uses: actions/checkout@v4 - - uses: JorisJonkers-dev/github-workflows/actions/setup-node@v0.6.0 - with: - node-version: '24' - package-manager: pnpm - cache-dependency-path: pnpm-lock.yaml - github-packages-token: ${{ secrets.GITHUB_TOKEN }} -``` - -Yarn example: - -```yaml -steps: - - uses: actions/checkout@v4 - - uses: JorisJonkers-dev/github-workflows/actions/setup-node@v0.6.0 - with: - node-version: '24' - package-manager: yarn - cache-dependency-path: yarn.lock - github-packages-token: ${{ secrets.GITHUB_TOKEN }} -``` - -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `node-version` | `24` | Node.js version installed by `actions/setup-node`. | -| `package-manager` | `pnpm` | Package manager to configure. Supported values: `npm`, `pnpm`, `yarn`. | -| `cache-dependency-path` | empty | Lockfile path or paths used for dependency caching. | -| `working-directory` | `.` | Directory where dependencies are installed. | -| `install-command` | empty | Overrides the install command. Empty uses `pnpm install --frozen-lockfile` or `yarn install --immutable`. | -| `github-packages-token` | empty | GitHub Packages token used to write project `.npmrc` auth and export `NODE_AUTH_TOKEN`. | -| `npm-scope` | `@jorisjonkers-dev` | npm scope resolved from GitHub Packages when `github-packages-token` is set. | -| `github-packages-registry` | `https://npm.pkg.github.com` | npm registry URL used for the configured GitHub Packages scope. | - -### `platform-config-validate` - -Installs a pinned `@jorisjonkers-dev/deploy-config-schema` package, validates caller -YAML configs, and can run `render-tree --check` to catch rendered-tree drift. - -```yaml -steps: - - uses: actions/checkout@v6 - - uses: JorisJonkers-dev/github-workflows/actions/platform-config-validate@v0.6.0 - with: - config-paths: |- - platform/**/*.yaml - platform/**/*.yml - schema-kind: auto - package-version: 0.3.0 - drift-check: 'true' -``` - -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `config-paths` | `platform/**/*.yaml`, `platform/**/*.yml` | Newline- or comma-separated file globs to validate. | -| `schema-kind` | `auto` | Schema kind: `platform`, `deploy-config`, `service-intent`, `fleet-inventory`, `vault-dynamic-secrets`, or `auto`. | -| `package-version` | `0.3.0` | Version of `@jorisjonkers-dev/deploy-config-schema` to install. | -| `drift-check` | `false` | Runs `render-tree --check` after validation when set to `true`. | -| `working-directory` | `.` | Directory where config globs are evaluated. | - -### `compose-system-test-stack` - -Runs a caller-owned Docker Compose CI/system-test stack, waits for service -health or routes, runs optional migration checks, dumps diagnostics on failure, -and tears the stack down by default. Compose files, service names, route URLs, -migration assertions, and hook commands stay in the consumer repository. - -```yaml -steps: - - uses: actions/checkout@v6 - - uses: JorisJonkers-dev/github-workflows/actions/compose-system-test-stack@v0.6.0 - with: - compose-files: |- - docker-compose.yml - docker-compose.ci.yml - services: api frontend db - wait-strategy: routes - wait-routes-file: .github/system-test-routes.txt - diagnostics-command: .github/scripts/dump-compose-diagnostics.sh - migration-check-command: .github/scripts/verify-migrations.sh -``` - -Route wait files contain blank lines, comments, or either `name url` or `url` -entries. Generate the file in the caller repository when URLs depend on -environment-specific hostnames or ports. - -The action tears the stack down before the next workflow step unless -`down-on-complete` is set to `false`. For tests that run in later steps, leave -the stack up and provide separate caller-owned diagnostics and cleanup. - -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `compose-files` | `docker-compose.yml`, `docker-compose.ci.yml` | Newline-separated compose files owned by the caller repository. | -| `services` | empty | Space-separated services to pass to `docker compose up`; empty starts the selected stack. | -| `project-name` | empty | Optional Docker Compose project name. | -| `working-directory` | `.` | Directory containing compose files and hook scripts. | -| `wait-strategy` | `compose-wait` | Wait mode: `compose-wait`, `services`, `command`, `routes`, or `none`. | -| `wait-command` | empty | Caller-owned command for `wait-strategy=command`. | -| `wait-routes-file` | empty | Caller-owned route list for `wait-strategy=routes`. | -| `wait-timeout-seconds` | `300` | Timeout per route when `wait-strategy=routes`. | -| `wait-interval-seconds` | `2` | Poll interval per route when `wait-strategy=routes`. | -| `migration-check-command` | empty | Optional caller-owned migration verification command. | -| `diagnostics-command` | empty | Optional caller-owned diagnostics command for failures. Built-in `ps`, logs, and Docker disk diagnostics run first. | -| `cleanup-command` | empty | Optional caller-owned cleanup command run before `docker compose down`. | -| `up-args` | `--no-build --wait --timeout 300 -d` | Extra `docker compose up` arguments. | -| `down-on-complete` | `true` | Whether to run `docker compose down --remove-orphans` after the stack step completes. | - -## Reusable workflows - -### `node-ci.yml` - -Runs dependency install plus optional package, lint, typecheck, test, and build -commands for Node repositories. +Reusable GitHub Actions workflows and composite actions for JorisJonkers-dev +repositories. + +## What It Is + +`github-workflows` centralizes CI, release, image publishing, deploy bundle, +Project automation, and repository hygiene automation. Consumer repositories +should call released tags instead of branches. + +## Composite Actions + +| Action | Purpose | +| --- | --- | +| `actions/setup-java-gradle` | Install Java and configure Gradle package access/cache settings. | +| `actions/setup-node` | Install Node, configure npm/pnpm/yarn caching, and install dependencies. | +| `actions/prepare-ci-host` | Download Docker image artifacts and prepare optional dev DNS. | +| `actions/platform-config-validate` | Install `@jorisjonkers-dev/deploy-config-schema` and validate platform YAML. | +| `actions/deploy-config-render-drift` | Render deploy-config adapter output and compare committed files. | +| `actions/flux-render-validate` | Validate Flux render output for GitOps repositories. | +| `actions/compose-system-test-stack` | Run caller-owned Docker Compose system-test stacks. | +| `actions/api-client-publish` | Generate and publish TypeScript, Java, and Kotlin API clients. | +| `actions/deploy-bundle` | Validate and pack a first-party `deploy/` directory as an OCI bundle. | +| `actions/deploy-sources-render` | Resolve deployment sources, compile Flux output, and emit image tags. | + +## Reusable Workflows + +| Workflow | Purpose | +| --- | --- | +| `node-ci.yml` | Node install, lint, typecheck, test, and build jobs. | +| `python-ci.yml` | Python lint/test workflow with overrideable commands. | +| `nix-ci.yml` | Nix installer plus flake check and optional validation. | +| `jvm-ci.yml` | Gradle lint/test workflow for JVM repositories. | +| `docker-image-ci.yml` | Build a Docker image without publishing. | +| `container-publish.yml` | Publish a GHCR image with release and sha tags. | +| `publish-api-clients.yml` | Generate and publish API clients from an OpenAPI spec. | +| `gitops-ci.yml` | Platform config, Flux render, drift, and optional system tests. | +| `platform-config-validate.yml` | Reusable platform YAML validation job. | +| `deploy-config-render-drift.yml` | Reusable deploy-config render-drift job. | +| `flux-render-validate.yml` | Reusable Flux render validation job. | +| `migration-guard.yml` | Block unsafe edits to existing Flyway-style migrations. | +| `crac-train.yml` | Build and run CRaC training images with optional sidecars. | +| `production-canary.yml` | Run caller-owned production smoke checks. | +| `deploy-bundle.yml` | Validate first-party deploy bundles and optionally publish them to GHCR. | +| `deploy-sources-render.yml` | Render deployment sources and expose image tags for downstream tests. | +| `repository-hygiene-guard.yml` | Block reintroduction of planning and scratch artifacts. | +| `add-to-project.yml` | Add opened/reopened issues and pull requests to the org Project. | + +## Examples ```yaml jobs: node-ci: - uses: JorisJonkers-dev/github-workflows/.github/workflows/node-ci.yml@v0.6.0 + uses: JorisJonkers-dev/github-workflows/.github/workflows/node-ci.yml@v0.7.3 with: package-manager: pnpm lint-command: pnpm lint - typecheck-command: pnpm typecheck test-command: pnpm test - build-command: pnpm build secrets: packages-token: ${{ secrets.GITHUB_TOKEN }} ``` -### `python-ci.yml` - -Runs standard Python install, lint, and test commands. Each command can be -overridden or disabled with an empty string. - -```yaml -jobs: - python-ci: - uses: JorisJonkers-dev/github-workflows/.github/workflows/python-ci.yml@v0.6.0 -``` - -### `nix-ci.yml` - -Installs Nix and runs a flake check plus an optional validation command. - ```yaml jobs: - nix-ci: - uses: JorisJonkers-dev/github-workflows/.github/workflows/nix-ci.yml@v0.6.0 -``` - -### `docker-image-ci.yml` - -Builds a Docker image without publishing it. - -```yaml -jobs: - image-ci: - uses: JorisJonkers-dev/github-workflows/.github/workflows/docker-image-ci.yml@v0.6.0 + deploy-bundle: + uses: JorisJonkers-dev/github-workflows/.github/workflows/deploy-bundle.yml@v0.7.3 with: - image-name: example-api - context: . - dockerfile: services/example-api/Dockerfile -``` - -### `container-publish.yml` - -Builds and publishes a GHCR image tagged only with the release version and -`sha-${GITHUB_SHA}`. - -```yaml -jobs: - publish: - uses: JorisJonkers-dev/github-workflows/.github/workflows/container-publish.yml@v0.6.0 - with: - image-name: example-api + deploy-dir: deploy version: ${{ needs.release.outputs.version }} -``` - -### `gitops-ci.yml` - -Runs platform config validation, Flux render validation, optional deploy-config -render drift, and optional caller-owned system tests. - -```yaml -jobs: - gitops-ci: - uses: JorisJonkers-dev/github-workflows/.github/workflows/gitops-ci.yml@v0.6.0 - with: - overlay-paths: |- - clusters/production - platform-config-paths: |- - inventory/**/*.yaml - inventory/**/*.yml + publish: true secrets: packages-token: ${{ secrets.GITHUB_TOKEN }} ``` -### `platform-config-validate.yml` - -Validates platform YAML from any consumer repository with one reusable workflow -job. This is the preferred entry point when the repository only needs the -standard validation job. - ```yaml jobs: - platform-config: - uses: JorisJonkers-dev/github-workflows/.github/workflows/platform-config-validate.yml@v0.6.0 - with: - config-paths: |- - platform/**/*.yaml - platform/**/*.yml - schema-kind: auto - package-version: 0.3.0 - drift-check: true -``` - -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `config-paths` | `platform/**/*.yaml`, `platform/**/*.yml` | Newline- or comma-separated file globs to validate. | -| `schema-kind` | `auto` | Schema kind: `platform`, `deploy-config`, `service-intent`, `fleet-inventory`, `vault-dynamic-secrets`, or `auto`. | -| `package-version` | `0.3.0` | Version of `@jorisjonkers-dev/deploy-config-schema` to install. | -| `drift-check` | `false` | Runs `render-tree --check` after validation when set to `true`. | -| `working-directory` | `.` | Directory where config globs are evaluated. | - -### `migration-guard.yml` - -Checks Flyway-style SQL migrations for two failure modes: existing migrations -changed after being committed, and newly added migrations whose version is not -greater than the base branch maximum for the same service. - -```yaml -jobs: - migration-guard: - uses: JorisJonkers-dev/github-workflows/.github/workflows/migration-guard.yml@v0.6.0 - with: - migration-regex: 'services/[^/]+/src/main/resources/db/migration(-pg)?/V[0-9][^/]*\.sql$' - scope-regex: '(services/[^/]+)/.*' - override-label: allow-migration-change + deploy-sources-render: + uses: JorisJonkers-dev/github-workflows/.github/workflows/deploy-sources-render.yml@v0.7.3 + secrets: + packages-token: ${{ secrets.GITHUB_TOKEN }} ``` -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `base-ref` | inferred PR base or default branch | Git ref used as the immutable migration baseline. | -| `migration-regex` | `services/.../db/migration(-pg)?/V*.sql` | Regex matching migration files. | -| `scope-regex` | `(services/[^/]+)/.*` | Regex whose first capture group defines the version namespace. | -| `override-label` | `allow-migration-change` | PR label that downgrades existing migration changes to warnings. | - -### `crac-train.yml` - -Builds CRaC training Docker targets, runs them with per-service optional -Postgres, Valkey, and RabbitMQ sidecars, validates that a checkpoint was -produced, and uploads one checkpoint artifact per service. Matrix rows that do -not declare `sidecars` keep the original round-2 behavior and start all three -sidecars. - ```yaml jobs: crac-train: - uses: JorisJonkers-dev/github-workflows/.github/workflows/crac-train.yml@v0.6.0 + uses: JorisJonkers-dev/github-workflows/.github/workflows/crac-train.yml@v0.7.3 with: service-matrix: >- [ { "service": "example-api", - "context": ".", "dockerfile": "services/example-api/Dockerfile", - "db_name": "example_db", - "db_user": "example_user", - "db_password": "example_password", - "port": 8080, "sidecars": ["postgres", "valkey"] }, { "service": "worker", - "context": ".", "dockerfile": "services/worker/Dockerfile", "sidecars": "none" } ] - docker-target: train - checkpoint-path: /opt/crac/checkpoint - expected-exit-codes: '0 137' - secrets: - packages-token: ${{ secrets.GITHUB_TOKEN }} ``` -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `service-matrix` | required | JSON matrix include list for services to train. | -| `docker-target` | `train` | Dockerfile target used for training images. | -| `checkpoint-path` | `/opt/crac/checkpoint` | Container path where CRaC writes checkpoint files. | -| `expected-exit-codes` | `0 137` | Space-separated accepted container exit codes. | -| `artifact-retention-days` | `7` | Checkpoint artifact retention period. | -| `postgres-image` | `postgres:17-alpine` | Postgres sidecar image. | -| `valkey-image` | `valkey/valkey:7-alpine` | Valkey sidecar image. | -| `rabbitmq-image` | `rabbitmq:3-management-alpine` | RabbitMQ sidecar image. | -| `extra-docker-run-args` | empty | Extra arguments appended before the training image tag. | - -Matrix fields: - -| Name | Default | Purpose | -| --- | --- | --- | -| `service` | required | Service name used for the local image tag and checkpoint artifact prefix. | -| `context` | `.` | Docker build context. | -| `dockerfile` | required | Dockerfile path in the caller repository. | -| `db_name` | `service` | Postgres database name when the Postgres sidecar is enabled. | -| `db_user` | `postgres` | Postgres username when the Postgres sidecar is enabled. | -| `db_password` | `postgres` | Postgres password when the Postgres sidecar is enabled. | -| `port` | empty | Optional `SERVER_PORT` passed to the training container. | -| `sidecars` | all three | String or list containing `postgres`, `valkey`, `rabbitmq`, or `none`. Missing keeps the backward-compatible full topology; `[]`, `none`, or `["none"]` starts no sidecars. | - -### `production-canary.yml` - -Runs a caller-owned production smoke command with optional post-push delay, -diagnostics, cleanup, and webhook notification. Service-specific URLs, auth, -assertions, and mutation behavior stay in the caller repository. +## Local Use -```yaml -jobs: - production-canary: - uses: JorisJonkers-dev/github-workflows/.github/workflows/production-canary.yml@v0.6.0 - with: - enabled: ${{ vars.PROD_CANARY_ENABLED == 'true' }} - post-push-delay-seconds: 180 - canary-command: ./scripts/prod-canary.sh - diagnostics-command: ./scripts/prod-canary-diagnostics.sh - cleanup-command: ./scripts/prod-canary-cleanup.sh - notification-message: Production canary failed. - secrets: - webhook-url: ${{ secrets.PROD_CANARY_WEBHOOK_URL }} +```bash +actionlint .github/workflows/*.yml +python3 -m unittest discover -s tests ``` -Inputs: - -| Name | Default | Purpose | -| --- | --- | --- | -| `enabled` | `true` | Allows callers to opt in through repository variables. | -| `canary-command` | required | Caller-owned smoke command. | -| `cleanup-command` | empty | Optional cleanup command run with `always()`. | -| `diagnostics-command` | empty | Optional diagnostics command run on failure. | -| `working-directory` | `.` | Directory where commands run. | -| `timeout-minutes` | `5` | Canary job timeout. | -| `post-push-delay-seconds` | `0` | Delay for push-triggered callers. | -| `notification-message` | `Production canary failed.` | Message prefix posted on failure. | - -### `jvm-ci.yml` - -Runs generic Gradle lint and test jobs for JVM repositories. - -```yaml -jobs: - jvm-ci: - uses: JorisJonkers-dev/github-workflows/.github/workflows/jvm-ci.yml@v0.6.0 - with: - java-version: '21' - gradle-args: --no-daemon --stacktrace - lint-gradle-args: detekt ktlintCheck - test-gradle-args: test - secrets: - packages-token: ${{ secrets.GITHUB_TOKEN }} -``` +## Links -Inputs: +- [Organization profile](https://github.com/JorisJonkers-dev) +- [Security policy](https://github.com/JorisJonkers-dev/.github/security/policy) +- [Changelog](./CHANGELOG.md) +- [License](./LICENSE) -| Name | Default | Purpose | -| --- | --- | --- | -| `java-version` | `21` | Java version installed for both jobs. | -| `java-distribution` | `temurin` | Java distribution installed for both jobs. | -| `working-directory` | `.` | Directory that contains the Gradle build. | -| `gradle-command` | `./gradlew` | Gradle command to run. | -| `gradle-args` | `--no-daemon` | Additional arguments appended to lint and test commands. | -| `lint-gradle-args` | `check` | Gradle tasks or arguments for lint checks. | -| `test-gradle-args` | `test` | Gradle tasks or arguments for tests. | -| `packages-token` | empty | GitHub Packages token passed to `setup-java-gradle` for Gradle package resolution. Prefer passing it as the `packages-token` workflow secret. | +Copyright (c) Joris Jonkers. Source available for viewing only; use, copying, +modification, redistribution, deployment, or reuse is not licensed. See +[LICENSE](./LICENSE). diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index f3ab91e..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,20 +0,0 @@ -# Security Policy - -## Reporting a vulnerability - -Report suspected vulnerabilities privately via GitHub Security Advisories -("Report a vulnerability" on the repository's Security tab), not as a public -issue. A maintainer will acknowledge and triage. - -## Secrets - -- No secrets in the repository. `gitleaks` runs in CI (part of `Pipeline - Complete`); a hit fails the pipeline. -- Runtime secrets come from Vault, never from committed files. -- Example/fixture credentials must be obviously non-real and live in - `*.example.*` files (see `.gitleaks.toml`). - -## Supported versions - -Only the latest released `vX.Y.Z` is supported. Pin exact versions (see -`VERSIONING.md`); do not depend on a moving branch. diff --git a/actions/api-client-publish/run.sh b/actions/api-client-publish/run.sh index ebfbd62..48dd458 100755 --- a/actions/api-client-publish/run.sh +++ b/actions/api-client-publish/run.sh @@ -157,6 +157,7 @@ write_jvm_build_gradle() { cat > "$output" <().configureEach { + options { + (this as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet") + } +} + publishing { publications { create("mavenJava") { diff --git a/actions/deploy-bundle/action.yml b/actions/deploy-bundle/action.yml new file mode 100644 index 0000000..6eebd95 --- /dev/null +++ b/actions/deploy-bundle/action.yml @@ -0,0 +1,68 @@ +name: 'Deploy Bundle' +description: 'Validate and pack a first-party deploy directory as an OCI deploy bundle.' + +inputs: + deploy-dir: + description: 'Path to the caller-owned deploy directory.' + required: false + default: 'deploy' + images: + description: 'Newline- or comma-separated image refs to include in the bundle manifest.' + required: false + default: '' + package-version: + description: 'Version range for @jorisjonkers-dev/deploy-config-schema.' + required: false + default: '^0.6.0' + version: + description: 'Bundle version. Defaults to the ref name or a sha-prefixed development version.' + required: false + default: '' + bundle-name: + description: 'Bundle package basename. Defaults to the repository name.' + required: false + default: '' + output-directory: + description: 'Directory where the bundle tarball is written.' + required: false + default: '' + working-directory: + description: 'Directory where deploy-dir is resolved.' + required: false + default: '.' + node-auth-token: + description: 'Token with packages:read for installing @jorisjonkers-dev packages from GitHub Packages.' + required: false + default: '' + +outputs: + bundle-path: + description: 'Path to the packed deploy bundle tarball.' + value: '${{ steps.bundle.outputs.bundle-path }}' + bundle-name: + description: 'Resolved deploy bundle name.' + value: '${{ steps.bundle.outputs.bundle-name }}' + bundle-version: + description: 'Resolved deploy bundle version.' + value: '${{ steps.bundle.outputs.bundle-version }}' + +runs: + using: 'composite' + steps: + - uses: 'actions/setup-node@v6' + with: + node-version: '24' + + - name: 'Validate and pack deploy bundle' + id: 'bundle' + shell: 'bash' + env: + DEPLOY_DIR: '${{ inputs.deploy-dir }}' + IMAGES: '${{ inputs.images }}' + PACKAGE_VERSION: '${{ inputs.package-version }}' + VERSION: '${{ inputs.version }}' + BUNDLE_NAME: '${{ inputs.bundle-name }}' + OUTPUT_DIRECTORY: '${{ inputs.output-directory }}' + WORKING_DIRECTORY: '${{ inputs.working-directory }}' + NODE_AUTH_TOKEN: '${{ inputs.node-auth-token }}' + run: 'bash "${{ github.action_path }}/run.sh"' diff --git a/actions/deploy-bundle/run.sh b/actions/deploy-bundle/run.sh new file mode 100755 index 0000000..e8f1121 --- /dev/null +++ b/actions/deploy-bundle/run.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +set -euo pipefail + +annotation() { + local kind="$1" + local message="$2" + printf '::%s::%s\n' "$kind" "$message" +} + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +install_schema_cli() { + local install_root="$1" + local version="$2" + local npmrc="${install_root}/.npmrc" + + rm -rf "$install_root" + mkdir -p "$install_root" + { + printf '%s\n' '@jorisjonkers-dev:registry=https://npm.pkg.github.com' + if [ -n "${NODE_AUTH_TOKEN:-}" ]; then + printf '%s\n' "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" + fi + } > "$npmrc" + + echo "::group::Install @jorisjonkers-dev/deploy-config-schema@${version}" >&2 + ( + cd "$install_root" + npm init -y >/dev/null + npm install --userconfig "$npmrc" --no-audit --no-fund --save-exact "@jorisjonkers-dev/deploy-config-schema@${version}" >&2 + ) + echo "::endgroup::" >&2 + + ( + cd "$install_root" + node - <<'NODE' +const path = require('path') +const packageRoot = path.resolve('node_modules/@jorisjonkers-dev/deploy-config-schema') +const manifest = require(path.join(packageRoot, 'package.json')) +const bin = manifest.bin + +let relativeBin = null +if (typeof bin === 'string') { + relativeBin = bin +} else if (bin && typeof bin === 'object') { + relativeBin = bin['deploy-config-schema'] || Object.values(bin)[0] +} + +if (!relativeBin) { + console.error('Package @jorisjonkers-dev/deploy-config-schema does not declare a CLI bin.') + process.exit(1) +} + +console.log(path.join(packageRoot, relativeBin)) +NODE + ) +} + +parse_images() { + local raw="$1" + raw="${raw//,/$'\n'}" + + IMAGE_ARGS=() + + local line + while IFS= read -r line || [ -n "$line" ]; do + line="${line//$'\r'/}" + line="$(trim "$line")" + if [ -z "$line" ] || [[ "$line" == \#* ]]; then + continue + fi + IMAGE_ARGS+=( --images "$line" ) + done <<< "$raw" +} + +main() { + local deploy_dir="${DEPLOY_DIR:-deploy}" + local images="${IMAGES:-}" + local package_version="${PACKAGE_VERSION:-^0.6.0}" + local version="${VERSION:-}" + local bundle_name="${BUNDLE_NAME:-}" + local output_directory="${OUTPUT_DIRECTORY:-}" + local working_directory="${WORKING_DIRECTORY:-.}" + + package_version="$(trim "$package_version")" + if [ -z "$package_version" ]; then + annotation error 'package-version is required.' + exit 1 + fi + if [ ! -d "$working_directory" ]; then + annotation error "working-directory does not exist: $working_directory" + exit 1 + fi + + if [ -z "$(trim "$version")" ]; then + version="${GITHUB_REF_NAME:-}" + version="${version#v}" + fi + if [ -z "$(trim "$version")" ]; then + version="0.0.0-sha-${GITHUB_SHA:-unknown}" + fi + + if [ -z "$(trim "$bundle_name")" ]; then + bundle_name="${GITHUB_REPOSITORY##*/}" + fi + if [ -z "$(trim "$bundle_name")" ]; then + annotation error 'bundle-name is required when GITHUB_REPOSITORY is unavailable.' + exit 1 + fi + + if [ -z "$(trim "$output_directory")" ]; then + output_directory="${RUNNER_TEMP:-/tmp}/deploy-bundle" + fi + + local install_root + install_root="${RUNNER_TEMP:-/tmp}/deploy-config-schema-cli" + local cli_bin + cli_bin="$(install_schema_cli "$install_root" "$package_version")" + + cd "$working_directory" + + if [ ! -d "$deploy_dir" ]; then + annotation error "deploy-dir does not exist: $deploy_dir" + exit 1 + fi + + mapfile -t deploy_files < <(find "$deploy_dir" -type f \( -name '*.yml' -o -name '*.yaml' \) | sort) + if [ "${#deploy_files[@]}" -eq 0 ]; then + annotation error "deploy-dir contains no YAML files: $deploy_dir" + exit 1 + fi + + local -a IMAGE_ARGS=() + parse_images "$images" + + mkdir -p "$output_directory" + local bundle_path="${output_directory%/}/${bundle_name}-${version}.tar" + + echo '::group::Validate deploy directory' + "$cli_bin" validate deployment-v2 "${deploy_files[@]}" + echo '::endgroup::' + + echo '::group::Pack deploy bundle' + "$cli_bin" bundle pack \ + --deploy-dir "$deploy_dir" \ + --repo "${GITHUB_REPOSITORY:-$bundle_name}" \ + --git-sha "${GITHUB_SHA:-unknown}" \ + --version "$version" \ + --out "$bundle_path" \ + "${IMAGE_ARGS[@]}" + echo '::endgroup::' + + if [ ! -s "$bundle_path" ]; then + annotation error "Bundle pack did not write a non-empty bundle: $bundle_path" + exit 1 + fi + + { + printf 'bundle-path=%s\n' "$bundle_path" + printf 'bundle-name=%s\n' "$bundle_name" + printf 'bundle-version=%s\n' "$version" + } >> "$GITHUB_OUTPUT" +} + +main "$@" diff --git a/actions/deploy-config-render-drift/run.sh b/actions/deploy-config-render-drift/run.sh index 1447007..1939602 100644 --- a/actions/deploy-config-render-drift/run.sh +++ b/actions/deploy-config-render-drift/run.sh @@ -26,7 +26,6 @@ install_schema_cli() { if [ -n "${NODE_AUTH_TOKEN:-}" ]; then printf '%s\n' "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" fi - printf '%s\n' 'always-auth=true' } > "${npmrc}" echo "::group::Install @jorisjonkers-dev/deploy-config-schema@${version}" >&2 diff --git a/actions/deploy-sources-render/action.yml b/actions/deploy-sources-render/action.yml new file mode 100644 index 0000000..aa281d1 --- /dev/null +++ b/actions/deploy-sources-render/action.yml @@ -0,0 +1,87 @@ +name: 'Deploy Sources Render' +description: 'Resolve deployment sources, compile Flux output, and check render drift.' + +inputs: + package-version: + description: 'Version range for @jorisjonkers-dev/deploy-config-schema.' + required: false + default: '^0.6.0' + environment: + description: 'Deployment environment to compile.' + required: false + default: 'production' + sources-path: + description: 'Path to deployment-sources.yml.' + required: false + default: 'deployment-sources.yml' + lock-path: + description: 'Path to deployment.lock.yml.' + required: false + default: 'deployment.lock.yml' + node-contract-path: + description: 'Path to the locked node contract.' + required: false + default: 'inventory/node-contract.lock.yml' + reachability-path: + description: 'Path to reachability catalog.' + required: false + default: 'catalog/reachability.yml' + output-path: + description: 'Rendered Flux output directory.' + required: false + default: 'cluster/flux' + working-directory: + description: 'Directory where paths are resolved.' + required: false + default: '.' + update-lock: + description: 'Run lock --update before compile.' + required: false + default: 'false' + check: + description: 'Require compiled output and derived lock files to be committed.' + required: false + default: 'true' + cutover-parity: + description: 'Run cutover parity against current-tree.' + required: false + default: 'false' + current-tree: + description: 'Current live Flux tree used when cutover-parity is true.' + required: false + default: '' + node-auth-token: + description: 'Token with packages:read for installing @jorisjonkers-dev packages from GitHub Packages.' + required: false + default: '' + +outputs: + image-tags: + description: 'Image tags resolved from deployment.lock.yml.' + value: '${{ steps.render.outputs.image-tags }}' + +runs: + using: 'composite' + steps: + - uses: 'actions/setup-node@v6' + with: + node-version: '24' + + - name: 'Render deployment sources' + id: 'render' + shell: 'bash' + env: + PACKAGE_VERSION: '${{ inputs.package-version }}' + ENVIRONMENT: '${{ inputs.environment }}' + SOURCES_PATH: '${{ inputs.sources-path }}' + LOCK_PATH: '${{ inputs.lock-path }}' + NODE_CONTRACT_PATH: '${{ inputs.node-contract-path }}' + REACHABILITY_PATH: '${{ inputs.reachability-path }}' + OUTPUT_PATH: '${{ inputs.output-path }}' + WORKING_DIRECTORY: '${{ inputs.working-directory }}' + UPDATE_LOCK: '${{ inputs.update-lock }}' + CHECK: '${{ inputs.check }}' + CUTOVER_PARITY: '${{ inputs.cutover-parity }}' + CURRENT_TREE: '${{ inputs.current-tree }}' + NODE_AUTH_TOKEN: '${{ inputs.node-auth-token }}' + run: 'bash "${{ github.action_path }}/run.sh"' diff --git a/actions/deploy-sources-render/run.sh b/actions/deploy-sources-render/run.sh new file mode 100755 index 0000000..314300d --- /dev/null +++ b/actions/deploy-sources-render/run.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +set -euo pipefail + +annotation() { + local kind="$1" + local message="$2" + printf '::%s::%s\n' "$kind" "$message" +} + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +normalize_bool() { + local name="$1" + local value + value="$(printf '%s' "${2:-false}" | tr '[:upper:]' '[:lower:]')" + case "$value" in + true|1|yes|y|on) + printf 'true' + ;; + false|0|no|n|off|'') + printf 'false' + ;; + *) + annotation error "Unsupported boolean for $name: ${2}. Use true or false." + exit 1 + ;; + esac +} + +install_schema_cli() { + local install_root="$1" + local version="$2" + local npmrc="${install_root}/.npmrc" + + rm -rf "$install_root" + mkdir -p "$install_root" + { + printf '%s\n' '@jorisjonkers-dev:registry=https://npm.pkg.github.com' + if [ -n "${NODE_AUTH_TOKEN:-}" ]; then + printf '%s\n' "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" + fi + } > "$npmrc" + + echo "::group::Install @jorisjonkers-dev/deploy-config-schema@${version}" >&2 + ( + cd "$install_root" + npm init -y >/dev/null + npm install --userconfig "$npmrc" --no-audit --no-fund --save-exact "@jorisjonkers-dev/deploy-config-schema@${version}" >&2 + ) + echo "::endgroup::" >&2 + + ( + cd "$install_root" + node - <<'NODE' +const path = require('path') +const packageRoot = path.resolve('node_modules/@jorisjonkers-dev/deploy-config-schema') +const manifest = require(path.join(packageRoot, 'package.json')) +const bin = manifest.bin + +let relativeBin = null +if (typeof bin === 'string') { + relativeBin = bin +} else if (bin && typeof bin === 'object') { + relativeBin = bin['deploy-config-schema'] || Object.values(bin)[0] +} + +if (!relativeBin) { + console.error('Package @jorisjonkers-dev/deploy-config-schema does not declare a CLI bin.') + process.exit(1) +} + +console.log(path.join(packageRoot, relativeBin)) +NODE + ) +} + +require_file() { + local path="$1" + local label="$2" + if [ ! -f "$path" ]; then + annotation error "$label does not exist: $path" + exit 1 + fi +} + +main() { + local package_version="${PACKAGE_VERSION:-^0.6.0}" + local environment="${ENVIRONMENT:-production}" + local sources_path="${SOURCES_PATH:-deployment-sources.yml}" + local lock_path="${LOCK_PATH:-deployment.lock.yml}" + local node_contract_path="${NODE_CONTRACT_PATH:-inventory/node-contract.lock.yml}" + local reachability_path="${REACHABILITY_PATH:-catalog/reachability.yml}" + local output_path="${OUTPUT_PATH:-cluster/flux}" + local working_directory="${WORKING_DIRECTORY:-.}" + local update_lock + local check + local cutover_parity + local current_tree="${CURRENT_TREE:-}" + + package_version="$(trim "$package_version")" + if [ -z "$package_version" ]; then + annotation error 'package-version is required.' + exit 1 + fi + if [ ! -d "$working_directory" ]; then + annotation error "working-directory does not exist: $working_directory" + exit 1 + fi + + update_lock="$(normalize_bool update-lock "${UPDATE_LOCK:-false}")" + check="$(normalize_bool check "${CHECK:-true}")" + cutover_parity="$(normalize_bool cutover-parity "${CUTOVER_PARITY:-false}")" + + local install_root + install_root="${RUNNER_TEMP:-/tmp}/deploy-config-schema-cli" + local cli_bin + cli_bin="$(install_schema_cli "$install_root" "$package_version")" + + cd "$working_directory" + + require_file "$sources_path" 'sources-path' + require_file "$lock_path" 'lock-path' + require_file "$node_contract_path" 'node-contract-path' + require_file "$reachability_path" 'reachability-path' + + echo '::group::Resolve deployment sources' + "$cli_bin" resolve-sources --sources "$sources_path" --lock "$lock_path" --check + echo '::endgroup::' + + if [ "$update_lock" = 'true' ]; then + echo '::group::Update deployment lock' + "$cli_bin" lock --sources "$sources_path" --lock "$lock_path" --update + echo '::endgroup::' + fi + + local -a compile_args=( + compile + --env "$environment" + --sources "$sources_path" + --lock "$lock_path" + --node-contract "$node_contract_path" + --reachability "$reachability_path" + --out "$output_path" + ) + if [ "$check" = 'true' ]; then + compile_args+=( --check ) + fi + + echo '::group::Compile deployment sources' + "$cli_bin" "${compile_args[@]}" + echo '::endgroup::' + + local image_tags + image_tags="$("$cli_bin" lock images --lock "$lock_path" --format image-tags)" + { + printf 'image-tags<> "$GITHUB_OUTPUT" + + if [ "$cutover_parity" = 'true' ]; then + if [ -z "$(trim "$current_tree")" ]; then + annotation error 'current-tree is required when cutover-parity is true.' + exit 1 + fi + echo '::group::Cutover parity' + "$cli_bin" parity --current "$current_tree" --rendered "$output_path" --allow-flux-source-diff true + echo '::endgroup::' + fi + + if [ "$check" = 'true' ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo '::group::Render drift' + git diff --exit-code -- "$lock_path" "$node_contract_path" "$reachability_path" "$output_path" + echo '::endgroup::' + fi +} + +main "$@" diff --git a/actions/platform-config-validate/run.sh b/actions/platform-config-validate/run.sh index c61fc42..2218bd6 100755 --- a/actions/platform-config-validate/run.sh +++ b/actions/platform-config-validate/run.sh @@ -55,7 +55,6 @@ install_schema_cli() { if [ -n "${NODE_AUTH_TOKEN:-}" ]; then printf '%s\n' "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" fi - printf '%s\n' 'always-auth=true' } > "$npmrc" echo "::group::Install @jorisjonkers-dev/deploy-config-schema@$version" >&2 diff --git a/actions/setup-node/action.yml b/actions/setup-node/action.yml index e8c2394..c16d935 100644 --- a/actions/setup-node/action.yml +++ b/actions/setup-node/action.yml @@ -59,7 +59,7 @@ runs: - name: Set up pnpm if: ${{ inputs.package-manager == 'pnpm' }} - uses: pnpm/action-setup@v5 + uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: diff --git a/specs/001-round2-reusable-workflows/plan.md b/specs/001-round2-reusable-workflows/plan.md deleted file mode 100644 index 313648d..0000000 --- a/specs/001-round2-reusable-workflows/plan.md +++ /dev/null @@ -1,43 +0,0 @@ -# Implementation Plan: Round 2 Reusable Workflows - -## Technical Approach - -This repository already publishes reusable GitHub Actions workflows and composite actions. The round-2 candidates fit that surface, so the implementation adds reusable `workflow_call` files under `.github/workflows/` and keeps executable logic small. - -The Flyway migration guard is implemented as `scripts/check_migrations.py` using only the Python standard library. A script is preferable to inline workflow shell because it is testable with fixtures, can expose configurable regex inputs, and can be covered by an 80% coverage gate. - -CRaC training remains a reusable workflow template. It builds and runs consumer-provided training targets with configurable sidecars and checkpoint validation. The workflow owns the repeatable CI mechanics; consumers own their service matrix and training profile. - -Production smoke/canary remains a reusable workflow template. It runs caller-provided commands for the real user journey, diagnostics, cleanup, and notification. This avoids extracting personal-stack endpoint paths, credentials, and mutation semantics. - -## Files - -- `.github/workflows/migration-guard.yml`: reusable Flyway migration guard. -- `.github/workflows/crac-train.yml`: reusable CRaC checkpoint training workflow. -- `.github/workflows/production-canary.yml`: reusable canary command runner. -- `scripts/check_migrations.py`: configurable migration validation CLI. -- `tests/test_check_migrations.py`: fixture-backed migration guard tests. -- `.github/workflows/ci.yml`: adds Python tests with `coverage --fail-under=80`. -- `README.md`: usage documentation for the new workflows. - -## Requirement Mapping - -- FR-001, FR-002, FR-003, FR-004: `scripts/check_migrations.py`. -- FR-005: `.github/workflows/migration-guard.yml`. -- FR-006, FR-007, FR-008: `.github/workflows/crac-train.yml`. -- FR-009, FR-010: `.github/workflows/production-canary.yml`. -- SC-001, SC-002, SC-003, SC-004: `tests/test_check_migrations.py`. -- SC-005, SC-006, SC-007: CRaC workflow inputs and matrix design. -- SC-008, SC-009, SC-010: canary workflow command and secret inputs. - -## Deviations - -The assignment mentions a branch named `impl/initial`, but this repo-specific task also requires `feat/round2`. The implementation uses `feat/round2` to match the assigned repository branch. - -CRaC sidecars are always present in this initial reusable workflow because the extracted personal-stack pattern depends on Postgres, Valkey, and RabbitMQ. Optional sidecar topology is a future enhancement if another consumer needs a smaller set. - -## Verification - -- Run `python3 -m unittest discover -s tests`. -- Run `python3 -m coverage run -m unittest discover -s tests && python3 -m coverage report --fail-under=80`. -- Run the existing CI YAML/actionlint validation locally where tool availability permits. diff --git a/specs/001-round2-reusable-workflows/spec.md b/specs/001-round2-reusable-workflows/spec.md deleted file mode 100644 index cb49217..0000000 --- a/specs/001-round2-reusable-workflows/spec.md +++ /dev/null @@ -1,60 +0,0 @@ -# Feature Specification: Round 2 Reusable Workflows - -## User Stories & Testing - -### User Story 1 - Guard Flyway migrations (Priority: P1) - -A repository with Flyway SQL migrations can call a reusable workflow that detects edits to already-committed migrations and rejects newly added migrations whose version is not greater than the base branch's highest version for the same service. - -**Success Criteria** - -- SC-001: A modified, deleted, or renamed existing migration fails unless the configured override is active. -- SC-002: A new migration with a version lower than or equal to the base branch maximum for that service fails. -- SC-003: Duplicate migration versions within a service fail, including split migration directories. -- SC-004: A repository with no matching migrations succeeds. - -### User Story 2 - Train CRaC checkpoints (Priority: P2) - -A JVM service repository can call a reusable workflow that builds one or more Dockerfile training targets, runs each image with Postgres, Valkey, and RabbitMQ sidecars, validates the CRaC checkpoint directory, and uploads checkpoint artifacts. - -**Success Criteria** - -- SC-005: Consumers provide the service matrix, Dockerfile target, checkpoint path, sidecar images, and artifact retention through inputs. -- SC-006: The workflow accepts the expected CRaC exit-code set and fails on unexpected non-zero exits. -- SC-007: The workflow does not contain personal-stack service names, database names, hostnames, or domains. - -### User Story 3 - Run production canaries (Priority: P3) - -A repository can call a reusable production canary workflow that checks out the caller repository, optionally waits after a main-branch push, runs a caller-owned smoke command, runs diagnostics and cleanup on failure or completion, and optionally posts to a webhook. - -**Success Criteria** - -- SC-008: Consumers provide the canary command and any service-specific authentication, URLs, assertions, cleanup, and mutation behavior. -- SC-009: The workflow supports scheduled/manual/push callers without embedding schedule details in the shared workflow. -- SC-010: Notification text and webhook secret are supplied by the consumer. - -## Functional Requirements - -- FR-001: The migration guard shall accept a base ref, migration path regex, service scope regex, and override flag. -- FR-002: The migration guard shall report GitHub Actions error annotations for immutable migration and ordering violations. -- FR-003: The migration guard shall support Flyway-style versions such as `V1__x.sql` and `V1_2__x.sql`. -- FR-004: The migration guard shall group versions by a configurable service scope. -- FR-005: The migration guard workflow shall be callable by other repositories and shall resolve the matching `github-workflows` ref before running the guard implementation. -- FR-006: The CRaC workflow shall accept a JSON matrix with per-service Docker context, Dockerfile, image tag, database credentials, and application port. -- FR-007: The CRaC workflow shall run with privileged Docker and host networking because CRIU checkpointing requires host capabilities. -- FR-008: The CRaC workflow shall upload one checkpoint artifact per matrix service. -- FR-009: The canary workflow shall run a caller-provided command and optional cleanup, diagnostics, and notification commands. -- FR-010: The canary workflow shall avoid storing production URLs, usernames, secrets, or endpoint paths in this repository. - -## Constraints - -- The shared workflows must not require personal-stack service names, domains, queue names, or database names. -- The migration guard is the only implementation code in this change and must keep at least 80% test coverage. -- CRaC and production canary workflows are templates; service-specific behavior stays in consumers. - -## Out of Scope - -- Rewriting full monorepo CI. -- Owning production canary business logic. -- Publishing an artifact package. -- Supporting non-GitHub CI systems. diff --git a/specs/001-round2-reusable-workflows/tasks.md b/specs/001-round2-reusable-workflows/tasks.md deleted file mode 100644 index 9d820fa..0000000 --- a/specs/001-round2-reusable-workflows/tasks.md +++ /dev/null @@ -1,9 +0,0 @@ -# Tasks: Round 2 Reusable Workflows - -- [x] T001 [FR-001..FR-004, SC-001..SC-004] Implement a configurable Flyway migration guard CLI. -- [x] T002 [FR-001..FR-004, SC-001..SC-004] Add fixture-backed tests for unchanged, modified, duplicate, and invalid new migrations. -- [x] T003 [FR-005] Add a reusable migration guard workflow that checks out `github-workflows` at the caller's ref. -- [x] T004 [FR-006..FR-008, SC-005..SC-007] Add a reusable CRaC training workflow with consumer-provided matrix inputs. -- [x] T005 [FR-009..FR-010, SC-008..SC-010] Add a reusable production canary workflow that runs caller-provided commands. -- [x] T006 [Constraint] Add CI coverage enforcement for the migration guard implementation. -- [x] T007 [Traceability] Document the reusable workflow inputs and examples in `README.md`. diff --git a/specs/002-round3-crac-sidecars-compose-stack/plan.md b/specs/002-round3-crac-sidecars-compose-stack/plan.md deleted file mode 100644 index 74cb67c..0000000 --- a/specs/002-round3-crac-sidecars-compose-stack/plan.md +++ /dev/null @@ -1,56 +0,0 @@ -# Implementation Plan: Round 3 CRaC Sidecars and Compose Stack Helpers - -## Technical Approach - -The round-2 CRaC reusable workflow used GitHub Actions job services, which are -defined at job creation time and are not a good fit for per-matrix optional -topology. The workflow now resolves `matrix.sidecars` in an early step and -starts requested sidecars with Docker only when needed. Training still runs with -host networking and privileged Docker so existing CRaC/CRIU assumptions remain -unchanged. - -Backward compatibility is preserved by treating a missing `sidecars` field as -the original full topology: Postgres, Valkey, and RabbitMQ. Consumers that need -less can opt in per matrix row. - -The Docker Compose CI/system-test helper is design-first. It is represented by -a spec, a guarded composite-action skeleton, and generic fixtures. The skeleton -documents the planned interface but intentionally does not run `docker compose` -or app-specific hooks. - -## Files - -- `.github/workflows/crac-train.yml`: per-matrix CRaC sidecar resolution, - conditional sidecar startup, conditional training environment variables, and - cleanup. -- `actions/compose-system-test-stack/action.yml`: design-first composite action - skeleton with planned inputs and a placeholder guard. -- `actions/compose-system-test-stack/fixtures/compose.stack.example.yml`: - generic fixture showing the intended compose-file shape. -- `actions/compose-system-test-stack/fixtures/routes.example.txt`: generic - route wait-list fixture. -- `tests/test_crac_train_workflow.py`: text-level workflow regression tests for - CRaC sidecar defaults, explicit `none`, validation, and README examples. -- `README.md`: updated CRaC matrix documentation and design-first Compose - helper documentation. - -## Requirement Mapping - -- FR-001, FR-002, FR-003, FR-004: `Resolve requested sidecars` step. -- FR-005: `Run training` environment argument assembly. -- FR-006: `actions/compose-system-test-stack/action.yml` and fixtures. -- SC-001..SC-005: `tests/test_crac_train_workflow.py`. -- SC-006..SC-008: spec, skeleton action, fixtures, and README. - -## Verification - -- Run `python3 -m unittest discover -s tests`. -- Run `python3 -m coverage run --source=scripts.check_migrations -m unittest discover -s tests`. -- Run `python3 -m coverage report --fail-under=80`. -- Run `actionlint .github/workflows/*.yml` when the binary is available. - -## Deviations - -The Compose system-test helper is intentionally not wired into CI and does not -perform Docker orchestration in this round because the assignment classifies it -as design-first only. diff --git a/specs/002-round3-crac-sidecars-compose-stack/spec.md b/specs/002-round3-crac-sidecars-compose-stack/spec.md deleted file mode 100644 index 28ce5b3..0000000 --- a/specs/002-round3-crac-sidecars-compose-stack/spec.md +++ /dev/null @@ -1,65 +0,0 @@ -# Feature Specification: Round 3 CRaC Sidecars and Compose Stack Helpers - -## User Stories & Testing - -### User Story 1 - Select CRaC sidecars per service (Priority: P1) - -A repository using the reusable CRaC training workflow can declare, per matrix -service, whether that service needs `postgres`, `valkey`, `rabbitmq`, or no -sidecars for checkpoint training. - -**Success Criteria** - -- SC-001: A matrix entry without `sidecars` keeps the round-2 behavior and - starts Postgres, Valkey, and RabbitMQ. -- SC-002: A matrix entry can set `sidecars` to a list such as - `["postgres", "valkey"]` and only those sidecars are started. -- SC-003: A matrix entry can set `sidecars` to `none`, `["none"]`, or `[]` and - no sidecars are started. -- SC-004: Unsupported sidecar names fail before training starts. -- SC-005: Shared workflow defaults do not contain consumer service names, - domains, hostnames, queue names, namespaces, image prefixes, or vendor URLs. - -### User Story 2 - Design a Compose system-test stack helper (Priority: P2) - -A repository maintainer can review a planned composite action surface for Docker -Compose CI/system-test stacks before a later production implementation. - -**Success Criteria** - -- SC-006: The action skeleton exposes inputs for compose files, service subsets, - project name, wait strategy, diagnostics, cleanup, and migration checks. -- SC-007: The action skeleton is clearly marked as design-first and cannot be - adopted accidentally as a working production helper. -- SC-008: Fixtures use generic service names and variable-based image names - rather than copied application values. - -## Functional Requirements - -- FR-001: The CRaC workflow shall accept `matrix.sidecars` as either a JSON - string or a JSON list. -- FR-002: Missing `matrix.sidecars` shall default to - `postgres,valkey,rabbitmq` for backward compatibility. -- FR-003: Explicit `none`, `["none"]`, or `[]` shall disable all sidecars. -- FR-004: `none` shall not be accepted when combined with another sidecar. -- FR-005: The training container shall only receive sidecar environment - variables for sidecars that were enabled. -- FR-006: The Compose helper skeleton shall define the planned composite action - inputs without implementing real `docker compose` orchestration this round. - -## Constraints - -- Do not modify `/workspace/personal-stack` or `/workspace/website`; they are - read-only reference repositories. -- Do not centralize application-specific compose files, route lists, migration - assertions, queue names, hostnames, IPs, domains, namespaces, Vault paths, or - image prefixes in this repository. -- Keep the existing CI shape, including the terminal `Pipeline Complete` job and - Python coverage gate. -- Avoid networked local verification; the external orchestrator runs full CI. - -## Out of Scope - -- Implementing a production Docker Compose stack runner in this round. -- Copying consumer compose files or app-specific bootstrap scripts. -- Adding a new package, service repository, or non-GitHub CI integration. diff --git a/specs/002-round3-crac-sidecars-compose-stack/tasks.md b/specs/002-round3-crac-sidecars-compose-stack/tasks.md deleted file mode 100644 index 04fd89c..0000000 --- a/specs/002-round3-crac-sidecars-compose-stack/tasks.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tasks: Round 3 CRaC Sidecars and Compose Stack Helpers - -- [x] T001 [FR-001..FR-004, SC-001..SC-004] Add CRaC matrix sidecar parsing with backward-compatible defaults. -- [x] T002 [FR-005, SC-002..SC-003] Start only requested CRaC sidecars and pass only matching training environment variables. -- [x] T003 [SC-005] Keep CRaC workflow defaults generic and free of reference-repo service values. -- [x] T004 [FR-006, SC-006..SC-008] Add a design-first Compose system-test stack action skeleton and generic fixtures. -- [x] T005 [Traceability] Document CRaC sidecar topology and the Compose helper skeleton in README. -- [x] T006 [Testing] Add Python harness tests for the CRaC workflow and skeleton action surface. diff --git a/specs/003-round4-compose-system-test-stack/plan.md b/specs/003-round4-compose-system-test-stack/plan.md deleted file mode 100644 index 9478f81..0000000 --- a/specs/003-round4-compose-system-test-stack/plan.md +++ /dev/null @@ -1,56 +0,0 @@ -# Implementation Plan: Round 4 Compose System-Test Stack Action - -## Technical Approach - -The Round 3 skeleton becomes a working composite action by delegating the -behavior to `actions/compose-system-test-stack/run.sh`. Keeping orchestration in -a shell runner makes the action YAML small enough for actionlint while allowing -the Python harness to execute the real control flow with stubbed `docker` and -`curl` commands. - -The action remains generic. It accepts compose files and hook commands from the -consumer repository, starts the requested service subset, waits via Compose -health checks, a caller command, route polling, or no extra wait, then optionally -runs caller-owned migration checks. Failure handling is centralized with a Bash -`EXIT` trap so diagnostics and cleanup run for startup, wait, and migration -failures. - -## Files - -- `actions/compose-system-test-stack/action.yml`: production composite action - inputs and runner invocation. -- `actions/compose-system-test-stack/run.sh`: compose argument assembly, - validation, startup, wait strategies, diagnostics, migration checks, and - cleanup. -- `actions/compose-system-test-stack/fixtures/compose.stack.example.yml`: - generic compose fixture with variable-based images. -- `actions/compose-system-test-stack/fixtures/routes.example.txt`: generic - route-wait fixture with caller-provided base URL placeholders. -- `tests/test_crac_train_workflow.py`: Python harness coverage for the action - surface and runner behavior with local stubs. -- `README.md`: working action documentation. - -## Requirement Mapping - -- FR-001, FR-002: compose file parsing, project name handling, and service - subset assembly in `run.sh`. -- FR-003: validation in `run.sh`. -- FR-004: route file parser and route polling in `run.sh`. -- FR-005: caller wait, migration, diagnostics, and cleanup command execution. -- FR-006: built-in diagnostics trap. -- FR-007: exit cleanup path. -- SC-001..SC-012: Python harness tests plus README/spec traceability. - -## Verification - -- Run `python3 -m unittest discover -s tests`. -- Run `python3 -m coverage run --source=scripts.check_migrations -m unittest discover -s tests`. -- Run `python3 -m coverage report --fail-under=80`. -- Run `actionlint .github/workflows/*.yml` and, when supported by the local - binary, lint composite action files too. - -## Deviations - -Docker Compose itself is not executed locally because the sandbox blocks Docker -sockets. The runner is verified through direct Bash execution with stubbed -commands, and the external orchestrator runs the full CI environment. diff --git a/specs/003-round4-compose-system-test-stack/spec.md b/specs/003-round4-compose-system-test-stack/spec.md deleted file mode 100644 index c8d8148..0000000 --- a/specs/003-round4-compose-system-test-stack/spec.md +++ /dev/null @@ -1,81 +0,0 @@ -# Feature Specification: Round 4 Compose System-Test Stack Action - -## User Stories & Testing - -### User Story 1 - Run a caller-owned Compose stack (Priority: P1) - -A repository maintainer can call the composite action with a list of compose -files, an optional Docker Compose project name, and an optional subset of -services to start for CI/system tests. - -**Success Criteria** - -- SC-001: The action no longer contains a design-first guard or placeholder - acknowledgement input. -- SC-002: Newline-separated compose files become ordered `docker compose -f` - arguments and are validated before startup. -- SC-003: `services` limits the `docker compose up` command to the requested - service names; an empty value starts the selected compose stack. -- SC-004: `project-name` is optional and, when set, is passed as `-p`. -- SC-005: The action and fixtures do not copy reference-repo compose files, - domains, hostnames, namespaces, queue names, IPs, image prefixes, or vendor - URLs. - -### User Story 2 - Wait for services or routes (Priority: P1) - -A caller can choose whether stack readiness is handled by Docker Compose health -checks, a caller command, route polling, or no additional wait. - -**Success Criteria** - -- SC-006: Supported `wait-strategy` values are `compose-wait`, `services`, - `command`, `routes`, and `none`. -- SC-007: `wait-strategy=command` requires and runs `wait-command`. -- SC-008: `wait-strategy=routes` requires `wait-routes-file` and polls each - non-comment route until it succeeds or the per-route timeout expires. - -### User Story 3 - Diagnose, migrate, and clean up (Priority: P2) - -When stack startup, wait, or migration verification fails, the action dumps -generic diagnostics, optionally runs caller diagnostics, and then cleans up. - -**Success Criteria** - -- SC-009: Built-in failure diagnostics include compose service state, compose - logs, Docker container state, and Docker disk usage. -- SC-010: `migration-check-command` runs after the configured wait strategy. -- SC-011: `cleanup-command` runs before `docker compose down`. -- SC-012: `down-on-complete=false` leaves teardown to the caller. - -## Functional Requirements - -- FR-001: The action shall start Docker Compose with caller-provided compose - file paths and no app-specific defaults beyond generic file names. -- FR-002: The action shall pass selected services to `docker compose up` only - when the `services` input is non-empty. -- FR-003: The action shall validate wait strategy, route wait timeout, route - wait interval, and boolean cleanup inputs before startup. -- FR-004: The action shall support route wait files with blank lines, comments, - `name url` entries, or single-URL entries. -- FR-005: The action shall run optional caller-owned wait, migration, - diagnostics, and cleanup commands from `working-directory`. -- FR-006: The action shall run generic built-in diagnostics on failures after - stack startup. -- FR-007: The action shall run `docker compose down --remove-orphans` on exit - when `down-on-complete` is `true`. - -## Constraints - -- Do not modify `/workspace/personal-stack` or `/workspace/website`; they are - read-only references. -- Do not copy consumer compose files, route lists, migration assertions, queue - names, hostnames, IPs, domains, namespaces, Vault paths, or image prefixes. -- Keep the existing CI shape, including the terminal `Pipeline Complete` job and - Python coverage gate. -- Avoid networked local verification; the external orchestrator runs full CI. - -## Out of Scope - -- Providing application-specific compose files, route inventories, bootstrap - services, or migration SQL assertions. -- Running Docker or networked CI locally in this sandbox. diff --git a/specs/003-round4-compose-system-test-stack/tasks.md b/specs/003-round4-compose-system-test-stack/tasks.md deleted file mode 100644 index 01914b4..0000000 --- a/specs/003-round4-compose-system-test-stack/tasks.md +++ /dev/null @@ -1,14 +0,0 @@ -# Tasks: Round 4 Compose System-Test Stack Action - -- [x] T001 [SC-001, FR-001] Remove the design-first placeholder guard and - expose a production composite action. -- [x] T002 [SC-002..SC-004, FR-001..FR-002] Implement compose file, project - name, and service subset command assembly. -- [x] T003 [SC-006..SC-008, FR-003..FR-004] Implement compose, command, routes, - services, and none wait strategies. -- [x] T004 [SC-009..SC-012, FR-005..FR-007] Implement diagnostics, migration - checks, cleanup hooks, and compose teardown. -- [x] T005 [SC-005] Keep fixtures generic and parameterized. -- [x] T006 [Testing] Extend the Python harness for the working action surface - and runner behavior. -- [x] T007 [Traceability] Document the working action contract in README. diff --git a/tests/test_crac_train_workflow.py b/tests/test_crac_train_workflow.py index 962cc2c..8dc4c11 100644 --- a/tests/test_crac_train_workflow.py +++ b/tests/test_crac_train_workflow.py @@ -21,6 +21,10 @@ DOCKER_IMAGE_CI_WORKFLOW = ROOT / ".github/workflows/docker-image-ci.yml" CONTAINER_PUBLISH_WORKFLOW = ROOT / ".github/workflows/container-publish.yml" GITOPS_CI_WORKFLOW = ROOT / ".github/workflows/gitops-ci.yml" +DEPLOY_BUNDLE_WORKFLOW = ROOT / ".github/workflows/deploy-bundle.yml" +DEPLOY_SOURCES_RENDER_WORKFLOW = ROOT / ".github/workflows/deploy-sources-render.yml" +ADD_TO_PROJECT_WORKFLOW = ROOT / ".github/workflows/add-to-project.yml" +HYGIENE_GUARD_WORKFLOW = ROOT / ".github/workflows/repository-hygiene-guard.yml" COMPOSE_ACTION = ROOT / "actions/compose-system-test-stack/action.yml" COMPOSE_RUNNER = ROOT / "actions/compose-system-test-stack/run.sh" COMPOSE_ROUTES_FIXTURE = ROOT / "actions/compose-system-test-stack/fixtures/routes.example.txt" @@ -504,7 +508,6 @@ def test_generic_fixtures_and_readme_usage_are_present(self) -> None: self.assertIn("name: example-platform", platform_fixture) self.assertIn("api-service:", service_fixture) self.assertIn("platform-config-validate.yml", self.readme) - self.assertIn("JorisJonkers-dev/github-workflows/.github/workflows/platform-config-validate.yml", self.readme) self.assertIn("@jorisjonkers-dev/deploy-config-schema", self.readme) def test_ci_uses_official_actionlint_download_script(self) -> None: @@ -522,21 +525,23 @@ def setUpClass(cls) -> None: cls.docker_image_ci = DOCKER_IMAGE_CI_WORKFLOW.read_text(encoding="utf-8") cls.container_publish = CONTAINER_PUBLISH_WORKFLOW.read_text(encoding="utf-8") cls.gitops_ci = GITOPS_CI_WORKFLOW.read_text(encoding="utf-8") + cls.deploy_bundle = DEPLOY_BUNDLE_WORKFLOW.read_text(encoding="utf-8") + cls.deploy_sources_render = DEPLOY_SOURCES_RENDER_WORKFLOW.read_text(encoding="utf-8") + cls.add_to_project = ADD_TO_PROJECT_WORKFLOW.read_text(encoding="utf-8") + cls.hygiene_guard = HYGIENE_GUARD_WORKFLOW.read_text(encoding="utf-8") cls.setup_node = SETUP_NODE_ACTION.read_text(encoding="utf-8") cls.readme = README.read_text(encoding="utf-8") - def test_new_reusable_workflows_are_documented_at_v0_6_0(self) -> None: + def test_reusable_workflows_are_documented_at_current_tag(self) -> None: for workflow in [ "node-ci.yml", - "python-ci.yml", - "nix-ci.yml", - "docker-image-ci.yml", - "container-publish.yml", - "gitops-ci.yml", + "deploy-bundle.yml", + "deploy-sources-render.yml", + "crac-train.yml", ]: self.assertTrue((ROOT / f".github/workflows/{workflow}").is_file()) self.assertIn( - f"JorisJonkers-dev/github-workflows/.github/workflows/{workflow}@v0.6.0", + f"JorisJonkers-dev/github-workflows/.github/workflows/{workflow}@v0.7.3", self.readme, ) @@ -571,11 +576,21 @@ def test_language_and_build_reusables_expose_expected_commands(self) -> None: def test_container_publish_tags_version_and_sha_only(self) -> None: self.assertIn("packages: write", self.container_publish) - self.assertIn("uses: docker/login-action@v3", self.container_publish) + self.assertIn("uses: docker/login-action@v4", self.container_publish) self.assertIn('printf \'%s:%s\\n\' "$image_ref" "$VERSION"', self.container_publish) self.assertIn('printf \'%s:sha-%s\\n\' "$image_ref" "$GITHUB_SHA"', self.container_publish) self.assertNotIn(":latest", self.container_publish) + def test_deploy_v2_workflows_expose_bundle_render_and_project_surfaces(self) -> None: + self.assertIn("'uses': './.github-workflows/actions/deploy-bundle'", self.deploy_bundle) + self.assertIn("oras push", self.deploy_bundle) + self.assertIn("application/vnd.jorisjonkers.deployment.bundle.v1+tar", self.deploy_bundle) + self.assertIn("'uses': './.github-workflows/actions/deploy-sources-render'", self.deploy_sources_render) + self.assertIn("'image-tags':", self.deploy_sources_render) + self.assertIn("actions/create-github-app-token@v3", self.add_to_project) + self.assertIn("actions/add-to-project@v2.0.0", self.add_to_project) + self.assertIn(".specify/**", self.hygiene_guard) + def test_gitops_ci_runs_required_and_optional_validation_steps(self) -> None: self.assertIn("uses: ./.github-workflows/actions/platform-config-validate", self.gitops_ci) self.assertIn("uses: ./.github-workflows/actions/flux-render-validate", self.gitops_ci)