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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions .github/workflows/git-ape-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
name: "Git-Ape: Static Validation"

# Runs every PR with parallel static-validation jobs. Complements (does not
# duplicate) the existing git-ape-actionlint, git-ape-docs-check, and
# git-ape-plugin-version-check workflows.
#
# Tools are pinned to specific versions to keep CI reproducible.

on:
pull_request:
paths:
- '**/*.sh'
- '**/*.bash'
- '**/*.bats'
- '**/*.yml'
- '**/*.yaml'
- '**/*.md'
- '**/*.json'
- 'schemas/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/git-ape-ci.yml'
push:
branches:
- main
paths:
- '**/*.sh'
- '**/*.bash'
- '**/*.bats'
- 'schemas/**'
- 'scripts/**'
- 'tests/**'

permissions:
contents: read

concurrency:
group: git-ape-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
# Pinned tool versions (bump intentionally; see schemas/README.md).
CHECK_JSONSCHEMA_VERSION: "0.37.2"
YAMLLINT_VERSION: "1.38.0"
MARKDOWNLINT_CLI_VERSION: "0.45.0"
BATS_VERSION: "1.11.1"
SHELLCHECK_VERSION: "0.10.0"

jobs:
# ---------------------------------------------------------------------------
# Bash linting
# ---------------------------------------------------------------------------
lint-shell:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install shellcheck (pinned)
run: |
set -euo pipefail
curl -fsSL "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" \
-o shellcheck.tar.xz
tar -xJf shellcheck.tar.xz
sudo mv "shellcheck-v${SHELLCHECK_VERSION}/shellcheck" /usr/local/bin/shellcheck
rm -rf "shellcheck-v${SHELLCHECK_VERSION}" shellcheck.tar.xz
shellcheck --version

- name: Strict shellcheck on new scripts (scripts/, tests/bash/)
run: |
set -euo pipefail
mapfile -t files < <(find scripts tests/bash -type f \
\( -name '*.sh' -o -name '*.bash' -o -name '*.bats' \) 2>/dev/null || true)
if [ "${#files[@]}" -eq 0 ]; then
echo "no new scripts to lint"
exit 0
fi
# bats files are bash; shellcheck reads them with -s bash.
shellcheck --severity=style --shell=bash "${files[@]}"

- name: Error-only shellcheck on existing scripts (.github/)
run: |
set -euo pipefail
# Existing scripts under .github/ predate this CI; we enforce only
# `error` severity here so genuine bugs fail CI without blocking on
# legacy SC2034/SC2045 warnings. A follow-up issue tracks cleanup.
mapfile -t files < <(find .github/scripts .github/skills -type f -name '*.sh' 2>/dev/null || true)
if [ "${#files[@]}" -eq 0 ]; then
echo "no existing scripts found"
exit 0
fi
shellcheck --severity=error "${files[@]}"

# ---------------------------------------------------------------------------
# YAML linting
# ---------------------------------------------------------------------------
lint-yaml:
name: yamllint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install yamllint (pinned)
run: pip install --no-cache-dir "yamllint==${YAMLLINT_VERSION}"

- name: Run yamllint
run: |
set -euo pipefail
# Advisory in this PR (|| true); follow-up will tighten and fail.
yamllint -c .yamllint.yml \
.github/workflows \
schemas \
scripts \
tests \
|| true

# ---------------------------------------------------------------------------
# Markdown linting
# ---------------------------------------------------------------------------
lint-markdown:
name: markdownlint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install markdownlint-cli (pinned)
run: npm install --no-fund --no-audit --global "markdownlint-cli@${MARKDOWNLINT_CLI_VERSION}"

- name: Run markdownlint (advisory)
run: |
set -euo pipefail
# Advisory-only in this PR. The existing repo has many docs that
# predate markdownlint; a follow-up will tighten and start failing.
markdownlint \
--ignore website/build \
--ignore website/node_modules \
--ignore '**/node_modules/**' \
--disable MD013 MD033 MD041 MD024 \
-- \
'README.md' \
'SECURITY.md' \
'schemas/**/*.md' \
'tests/**/*.md' \
|| true

# ---------------------------------------------------------------------------
# JSON Schema validation
# ---------------------------------------------------------------------------
validate-schemas:
name: JSON Schema validation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install check-jsonschema (pinned)
run: pip install --no-cache-dir "check-jsonschema==${CHECK_JSONSCHEMA_VERSION}"

- name: Meta-validate schemas
run: |
set -euo pipefail
for schema in schemas/git-ape/_defs/v1.json \
schemas/git-ape/state/v1.json \
schemas/git-ape/metadata/v1.json \
schemas/git-ape/security-gate/v1.json \
schemas/git-ape/requirements/v1.json \
schemas/git-ape/cost-estimate/v1.json \
schemas/git-ape/policy-recommendations/v1.json \
schemas/git-ape/plugin/v1.json; do
check-jsonschema --check-metaschema "$schema"
done

- name: Bulk validate fixtures + plugin.json
run: bash scripts/validate-schemas.sh

# ---------------------------------------------------------------------------
# bats-core test suite
# ---------------------------------------------------------------------------
bats-tests:
name: bats
runs-on: ubuntu-latest
needs: validate-schemas
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install check-jsonschema (pinned)
run: pip install --no-cache-dir "check-jsonschema==${CHECK_JSONSCHEMA_VERSION}"

- name: Install bats-core (pinned)
run: |
set -euo pipefail
curl -fsSL "https://github.com/bats-core/bats-core/archive/refs/tags/v${BATS_VERSION}.tar.gz" \
-o bats.tgz
tar -xzf bats.tgz
sudo "bats-core-${BATS_VERSION}/install.sh" /usr/local
rm -rf "bats-core-${BATS_VERSION}" bats.tgz
bats --version

- name: Run bats suite
run: bats tests/bash
23 changes: 17 additions & 6 deletions .github/workflows/git-ape-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,28 @@ jobs:
TAG: ${{ steps.ver.outputs.tag }}
run: |
set -euo pipefail
PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || echo "")

# Use the tag as the tip of the range when it exists as a ref. On
# workflow_dispatch with versions already aligned, the prior
# "Commit version bump" step is skipped, so the tag does not yet
# exist locally — fall back to HEAD so git log still walks the
# right commits.
if git rev-parse --verify "$TAG" >/dev/null 2>&1; then
TIP="$TAG"
else
TIP="HEAD"
fi

PREV_TAG=$(git describe --tags --abbrev=0 "${TIP}^" 2>/dev/null || echo "")

# Collect commits grouped by conventional-commit type
if [[ -n "$PREV_TAG" ]]; then
RANGE="$PREV_TAG..$TAG"
RANGE="${PREV_TAG}..${TIP}"
else
RANGE="$TAG"
RANGE="$TIP"
fi

declare -A SECTIONS
SECTIONS=(
declare -A SECTIONS=(
[feat]=""
[fix]=""
[docs]=""
Expand All @@ -137,7 +148,7 @@ jobs:
[other]=""
)

SECTION_TITLES=(
declare -A SECTION_TITLES=(
[feat]="Features"
[fix]="Bug Fixes"
[docs]="Documentation"
Expand Down
17 changes: 17 additions & 0 deletions .yamllint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# yamllint config used by .github/workflows/git-ape-ci.yml.
# Kept intentionally permissive in this PR; future PRs may tighten rules.
extends: default

rules:
line-length: disable
comments-indentation: disable
document-start: disable
truthy:
allowed-values: ['true', 'false', 'on']
indentation:
spaces: 2
indent-sequences: consistent
braces:
max-spaces-inside: 1
brackets:
max-spaces-inside: 1
106 changes: 106 additions & 0 deletions schemas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Git-Ape JSON Schemas

This directory holds the authoritative JSON Schemas (draft 2020-12) for every
JSON artifact Git-Ape emits. Schemas are versioned per artifact and validated
in CI on every PR.

## Layout

```
schemas/
├── README.md ← you are here
└── git-ape/
├── _defs/
│ └── v1.json ← canonical shared types
├── state/v1.json ← state.json artifact
├── metadata/v1.json ← metadata.json artifact
├── requirements/v1.json ← requirements.json artifact
├── security-gate/v1.json ← security-gate.json artifact
├── cost-estimate/v1.json ← cost-estimate.json artifact
├── policy-recommendations/v1.json ← policy-recommendations.json artifact
└── plugin/v1.json ← plugin.json (this repo's manifest)
```

## Versioning policy

- **Per-artifact `schemaVersion`.** Every artifact carries its own
`schemaVersion` field. The shared `$defs` in `_defs/v1.json` are versioned
together as a release train: a new `_defs/v2.json` lands when at least one
artifact graduates to v2.
- **Major bumps for breaking changes.** Renaming, removing, or retyping a
documented field. Migrate the in-tree corpus and emitter scripts in the same
PR.
- **Minor bumps for additive changes.** Adding a new optional field, relaxing a
pattern, broadening an enum. Old documents stay valid against the new schema.
- **Patch bumps are not used.** JSON Schema documents do not need patch
semantics — descriptive changes go in via PR without a version bump.

## Strictness rules

- `additionalProperties: false` on every top-level artifact object.
- Nested objects (e.g. `managedResources[]` items) allow extras during the
transition — they will tighten in a future minor.
- `$schema` is whitelisted as a known top-level extension key on every
artifact so editors can attach a schema without failing validation.
- Required fields are explicit. Anything not in `required` is optional and
must be omitted rather than set to `null` unless the schema documents
`null` as a valid type.

## How emitters reference schemas

When an emitter (e.g. a future `deploy-stack.sh`) writes an artifact, it
SHOULD set a relative `$schema` field pointing at the schema for that
artifact:

```json
{
"$schema": "../../../schemas/git-ape/state/v1.json",
"schemaVersion": "1.0",
"deploymentId": "deploy-20260506-001"
}
```

The relative path is resolved against the artifact's location. Editors with
JSON Schema support will auto-validate the file with no configuration. CI
ignores this field — `scripts/validate-schemas.sh` selects the schema based
on the file name.

## How CI uses these schemas

`.github/workflows/git-ape-ci.yml` invokes `scripts/validate-schemas.sh` on
every PR. The script:

1. Walks `.azure/deployments/**/*.json` and `tests/fixtures/**/*.json`.
2. Selects the schema for each file by its base name (`state.json`,
`metadata.json`, …).
3. Runs `check-jsonschema --schemafile <schema> <file>`.
4. Fails the job if any file violates its schema.

Negative-test fixtures live under `tests/fixtures/_invalid/` and are
deliberately rejected by the corresponding bats test, not the bulk
validator.

## Adding or evolving a schema

1. Decide major vs minor (see "Versioning policy").
2. Author or copy the schema file.
3. Update `tests/fixtures/<artifact>/` with a valid sample and (when adding
a new constraint) a negative sample under `tests/fixtures/_invalid/`.
4. Run `scripts/validate-schemas.sh` locally.
5. Update `tests/bash/schema-validation.bats` if you added a new artifact.
6. If you are introducing a breaking change, also add a migration step in a
follow-up PR and a one-line note in the artifact's `description`.

## Known follow-ups

- **Deduplicate `$defs` via cross-file `$ref`.** Today every per-artifact
schema embeds its own copy of the shared types from `_defs/v1.json`. This
keeps `check-jsonschema` invocations trivially portable. A follow-up will
switch to bundled `$ref`s using a single registry document. Tracked
alongside the doc-generation work.
- **`requirements.json` `resources[].configuration` discrimination.** The
v1.0 schema accepts any object; a v1.1 will use `oneOf` keyed on `type` so
per-resource shapes are validated.
- **SchemaStore.org submission.** Once the registry stabilises, submit a PR
to `SchemaStore/schemastore` so editors discover Git-Ape artifacts by file
pattern with no workspace setup.
Loading
Loading