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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions .github/workflows/self-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# SPDX-License-Identifier: MPL-2.0
# Self-test — this action run against its own fixture corpus.
#
# Why this workflow exists
# ------------------------
# Until v1.0.0 this action had no self-test at all, and its own tree contained
# not one `.deed` file. Its discovery glob named `*.a2ml` only, so a repository
# holding nothing but `.deed` manifests scanned zero files, reported zero
# errors and exited 0 — a silent total bypass that a green tick concealed.
#
# Two rules follow from that, and both are load-bearing here:
#
# 1. ASSERT ON DISCOVERY COUNT, NEVER ON EXIT CODE. A validator that finds
# nothing exits 0. Checking only the exit code cannot tell "all clean"
# apart from "saw nothing at all" — which is precisely the defect that
# shipped.
# 2. ALWAYS RUN A POSITIVE CONTROL. The `invalid` job must report non-zero
# errors. If it ever reports zero, the run has stopped being able to see
# failure, and every other green result in this workflow is worthless.

name: Self-test

on:
pull_request:
branches: ['**']
push:
branches: [main, master]
workflow_dispatch:

permissions:
contents: read

jobs:
# ---------------------------------------------------------------------------
# Valid corpus — every accepted shape, including the two that broke.
# ---------------------------------------------------------------------------
valid-corpus:
name: valid corpus (strict)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- id: validate
uses: ./
with:
path: test/fixtures/valid
strict: 'true'
- name: Assert 5 files discovered and all clean
env:
SCANNED: ${{ steps.validate.outputs.files-scanned }}
ERRORS: ${{ steps.validate.outputs.errors }}
WARNINGS: ${{ steps.validate.outputs.warnings }}
run: |
set -euo pipefail
# The count is pinned deliberately. Adding a fixture must be a
# conscious edit here, so that silent UNDER-discovery cannot pass.
if [ "${SCANNED}" != "5" ]; then
echo "::error::expected 5 files discovered, got ${SCANNED}"
exit 1
fi
if [ "${ERRORS}" != "0" ] || [ "${WARNINGS}" != "0" ]; then
echo "::error::valid corpus must be clean, got ${ERRORS} error(s) and ${WARNINGS} warning(s)"
exit 1
fi
echo "valid corpus: ${SCANNED} discovered, clean"

# ---------------------------------------------------------------------------
# Invalid corpus — THE POSITIVE CONTROL. This job proves the run is capable
# of reporting a non-zero error count. Without it, every green above is
# unfalsifiable.
#
# Two of the three fixtures are token-boundary controls: `(versioning` and
# `:registry-version` must NOT satisfy the version check. Removing the `^`
# anchors in v1.0.0 widened what matches, and these fixtures are what stop
# that widening going too far.
# ---------------------------------------------------------------------------
invalid-corpus:
name: invalid corpus (positive control)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- id: validate
continue-on-error: true # the action is SUPPOSED to fail here
uses: ./
with:
path: test/fixtures/invalid
strict: 'true'
- name: Assert every invalid fixture was caught
env:
SCANNED: ${{ steps.validate.outputs.files-scanned }}
ERRORS: ${{ steps.validate.outputs.errors }}
run: |
set -euo pipefail
if [ "${SCANNED}" != "3" ]; then
echo "::error::expected 3 files discovered, got ${SCANNED}"
exit 1
fi
# Not merely "> 0": every fixture in this directory is invalid, so a
# count below 3 means one of them was wrongly accepted.
if [ "${ERRORS}" != "3" ]; then
echo "::error::positive control failed — expected 3 errors, got ${ERRORS}"
echo "::error::if this reads 0, the run can no longer detect failure at all"
exit 1
fi
echo "invalid corpus: ${SCANNED} discovered, ${ERRORS} correctly rejected"

# ---------------------------------------------------------------------------
# Discovery control — a directory containing NOT ONE `.a2ml` file.
#
# This is the regression test for the original defect. Against the pre-v1.0.0
# glob this job reports 0 files scanned and 0 errors, and a naive exit-code
# check would call that a pass.
# ---------------------------------------------------------------------------
deed-only-discovery:
name: .deed-only discovery (regression)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- id: validate
uses: ./
with:
path: test/fixtures/deed-only
strict: 'true'
- name: Assert the .deed file was actually seen
env:
SCANNED: ${{ steps.validate.outputs.files-scanned }}
ERRORS: ${{ steps.validate.outputs.errors }}
run: |
set -euo pipefail
if [ "${SCANNED}" != "1" ]; then
echo "::error::expected 1 file discovered, got ${SCANNED}"
echo "::error::0 here means the glob has stopped matching .deed — the v1.0.0 defect has returned"
exit 1
fi
if [ "${ERRORS}" != "0" ]; then
echo "::error::expected the .deed fixture to validate cleanly, got ${ERRORS} error(s)"
exit 1
fi
echo "discovery control: ${SCANNED} .deed file seen and validated"

# ---------------------------------------------------------------------------
# Shell lint on the validator itself.
# ---------------------------------------------------------------------------
shellcheck:
name: shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Lint validator scripts
run: |
set -euo pipefail
bash -n validate-a2ml.sh
bash -n validate-manifest-dialect.sh
shellcheck -S warning validate-a2ml.sh validate-manifest-dialect.sh
56 changes: 56 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,59 @@ Changelog], and this project adheres to
https://semver.org/spec/v2.0.0.html[Semantic Versioning].

=== [Unreleased]

=== [1.0.0] -- 2026-09-15

First tagged release, and the first release in which the action can actually
see a `.deed` file.

==== Added

* `.deed` discovery. The file-discovery glob now matches `*.deed` alongside the
superseded `*.a2ml`. Before this release the glob named `*.a2ml` only, so a
repository containing nothing but `.deed` manifests was scanned, found zero
files, reported zero errors and exited `0` -- a silent total bypass rather
than a visible failure.
* Recognition of the `:canonical-name`, `:estate-authority` and `:agent-id`
identity keys, and of `:schema-version` as a version key.
* Recognition of identity by head form alone for the four ruled DEED heads
(`estate-deed`, `estate-atlas-deed`, `repo-deed`, `praxis-deed`), which is how
`ATLAS.deed` identifies itself -- it carries no `:canonical-name`.
* A self-test workflow (`.github/workflows/self-test.yml`) and a fixture corpus
under `test/fixtures/`. The action now runs against its own tree, which it
never did before -- the repository contained not one `.deed` file, so nothing
the action did was ever exercised against the format it is named for. The
jobs assert on discovery *count* rather than exit code, because a validator
that finds nothing exits `0`; the `invalid/` corpus is a positive control
proving the run can still report failure; and two fixtures pin the token
boundary so that removing the `^` anchors cannot widen too far.

==== Fixed

* Nested `(metadata ...)` and `(version ...)` forms written *inline* were not
detected. Both checks were anchored with `^[[:space:]]*`, so a manifest
written as `(state (metadata (name "x") (version "1.0.0")))` reported
"No identity found" and "Missing version" -- and under `strict: true` that
rejected a valid manifest. The anchors are gone; the `(` prefix and the
trailing whitespace/EOL boundary are retained, so `(versioning` and
`:registry-version` still correctly fail the version check.
* `README.adoc`, which is the GitHub Marketplace landing page, carried three
dead or wrong references: the DEED specification link pointed at
`standards/tree/main/a2ml` (a 404), the usage snippet told users to write
`uses: hyperpolymath/standards/a2ml/actions/validate@main` (a different
repository entirely), and the ecosystem section used a monorepo-relative
`link:../../README.adoc` that resolves nowhere from a standalone repo.

==== Changed

* Prose and examples throughout `README.adoc` and `action.yml` now speak of DEED
and `.deed`, with `.a2ml` referred to as superseded rather than as the primary
format. A2ML is expanded as "Attestation Markup Language".

==== Known limitations

* Roughly 120 stamped copies of this validator exist elsewhere on a separate
lineage. They still carry the old glob and are not reached by fixing this
repository.
* The Check-3 attestation exemption still matches bare basenames rather than
canonical paths.
33 changes: 17 additions & 16 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>

= Validate A2ML Manifests -- GitHub Action
= Validate DEED Manifests -- GitHub Action
:author: Jonathan D.A. Jewell
:toc: preamble
:icons: font

== Overview

GitHub Action that scans a repository for `.a2ml` files and validates their structure,
required fields, and compliance with the
https://github.com/hyperpolymath/standards/tree/main/a2ml[A2ML specification].
GitHub Action that scans a repository for `.deed` files -- and the superseded `.a2ml`
extension -- and validates their structure, required fields, and compliance with the
https://github.com/hyperpolymath/standards/tree/main/deed[DEED specification].

A2ML (Attested Markup Language) is the manifest format used across
DEED -- which supersedes A2ML (Attestation Markup Language) -- is the manifest format used across
https://github.com/hyperpolymath/standards[RSR (Rhodium Standard Repository)] projects to
declare machine-readable metadata, AI agent instructions, attestation provenance, and
project state.
Expand Down Expand Up @@ -47,18 +47,18 @@ Add this step to any GitHub Actions workflow:

[source,yaml]
----
name: Validate A2ML
name: Validate DEED
on: [push, pull_request]

permissions:
contents: read

jobs:
validate-a2ml:
validate-deed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hyperpolymath/standards/a2ml/actions/validate@main
- uses: hyperpolymath/deed-validate-action@v1
with:
path: '.' # Directory to scan (default: repo root)
strict: 'false' # Promote warnings to errors (default: false)
Expand All @@ -72,12 +72,12 @@ jobs:

| `path`
| `.`
| Directory path to scan for `.a2ml` files. The scan is recursive, excluding `.git/`.
| Directory path to scan for `.deed` files (and superseded `.a2ml`). The scan is recursive, excluding `.git/`.

| `strict`
| `false`
| When `true`, all warnings are promoted to errors and the action fails on any validation
issue. Recommended for repositories that require full A2ML compliance.
issue. Recommended for repositories that require full DEED compliance.
|===

=== Outputs
Expand All @@ -87,7 +87,7 @@ jobs:
| Output | Description

| `files-scanned`
| Number of `.a2ml` files discovered and processed.
| Number of DEED-family files (`.deed` and superseded `.a2ml`) discovered and processed.

| `errors`
| Count of validation errors. The action exits with code 1 if this is non-zero.
Expand All @@ -103,7 +103,7 @@ jobs:
| Code | Meaning

| `0`
| All files valid (or only warnings in non-strict mode). Also returned when no `.a2ml`
| All files valid (or only warnings in non-strict mode). Also returned when no DEED-family
files are found (with a `::notice::` annotation).

| `1`
Expand Down Expand Up @@ -132,8 +132,9 @@ SPDX-License-Identifier: MPL-2.0

See link:LICENSE[LICENSE] for the full text.

== Part of the A2ML Ecosystem
== Part of the DEED Ecosystem

This action is part of the link:../../README.adoc[A2ML specification and tooling] in the
https://github.com/hyperpolymath/standards[standards monorepo]. See the parent directory
for language bindings, Pandoc support, editor integrations, and the CLI.
This action is part of the https://github.com/hyperpolymath/deed-ecosystem[DEED specification
and tooling]. The normative grammar lives in the
https://github.com/hyperpolymath/standards/tree/main/deed[standards monorepo], with language
bindings, Pandoc support, editor integrations, and the CLI.
21 changes: 11 additions & 10 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# action.yml — Validate A2ML Manifests GitHub Action
# Scans repository for .a2ml files and validates structure, required fields,
# SPDX headers, and attestation blocks.
# action.yml — Validate DEED Manifests GitHub Action
# Scans repository for .deed (and superseded .a2ml) files and validates
# structure, required fields, SPDX headers, and attestation blocks.

name: 'Validate A2ML Manifests'
name: 'Validate DEED Manifests'
description: >-
Scan and validate .a2ml manifest files in your repository.
Checks for required fields (agent-id/pedigree name, version),
SPDX headers, and attestation block structure.
Scan and validate .deed manifest files against the DEED grammar.
Checks identity (:canonical-name), version (:schema-version),
SPDX headers, and attestation block structure. Also reads the
superseded .a2ml extension.
author: 'Jonathan D.A. Jewell'

branding:
Expand All @@ -19,7 +20,7 @@ branding:
inputs:
path:
description: >-
Directory path to scan for .a2ml files.
Directory path to scan for .deed files (and superseded .a2ml).
Comment thread
hyperpolymath marked this conversation as resolved.
Defaults to the repository root.
required: false
default: '.'
Expand All @@ -32,7 +33,7 @@ inputs:

outputs:
files-scanned:
description: 'Number of .a2ml files scanned'
description: 'Number of DEED-family files scanned'
value: ${{ steps.validate.outputs.files_scanned }}
errors:
description: 'Number of validation errors found'
Expand All @@ -44,7 +45,7 @@ outputs:
runs:
using: 'composite'
steps:
- name: Validate A2ML manifests
- name: Validate DEED manifests
id: validate
shell: bash
env:
Expand Down
Loading
Loading