From 073e86fec8d55f045f1a56a6c264d0ce7638d6d5 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 21 Jun 2026 19:09:14 +0200 Subject: [PATCH 1/3] feat(ci): add composite actions for reqstool validate/status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate composite actions for the OpenSpec dogfooding rollout (reqstool/PLAN_dog_fooding.md), each installing reqstool from either PyPI or reqstool-client@main: - validate-reqstool: runs `reqstool validate --strict` (spec completeness — every requirement has SVCs, manual SVCs have MVRs). - reqstool-status: runs `reqstool status --verbosity compact`, optionally gated on `--check-all-reqs-met` via fail-if-incomplete (left off by default for repos that intentionally have incomplete requirements, e.g. demo/fixture repos). These are composite actions, not reusable workflows (unlike common-validate-openspec.yml, #40): they run as steps within the calling job, after that job's own build step, since reqstool status/validate needs build-time artifacts (annotations.yml, test results) that a separate reusable-workflow job wouldn't have access to. First consumer: reqstool-demo#104. Signed-off-by: Jimisola Laursen --- .github/actions/reqstool-status/action.yml | 46 ++++++++++++++++++++ .github/actions/validate-reqstool/action.yml | 37 ++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 .github/actions/reqstool-status/action.yml create mode 100644 .github/actions/validate-reqstool/action.yml diff --git a/.github/actions/reqstool-status/action.yml b/.github/actions/reqstool-status/action.yml new file mode 100644 index 0000000..bf6446d --- /dev/null +++ b/.github/actions/reqstool-status/action.yml @@ -0,0 +1,46 @@ +name: "reqstool status" +description: "Install reqstool (from PyPI or reqstool-client@main) and run reqstool status against a reqstool data path. By default just reports status; set fail-if-incomplete to gate CI on every requirement being complete. Must run after the repo's own build step, in the same job, since it depends on build-time artifacts (e.g. annotations.yml, test results)." + +inputs: + reqstool-source: + description: "Where to install reqstool from: 'pypi' (latest release) or 'main' (reqstool-client@main)." + required: false + default: "pypi" + reqstool-path: + description: "Path to the reqstool data directory (containing requirements.yml etc.), relative to repo root." + required: false + default: "docs/reqstool" + fail-if-incomplete: + description: "Fail the step unless all requirements are implemented (passes --check-all-reqs-met). Leave false for repos that intentionally have incomplete requirements (e.g. demo/fixture repos)." + required: false + default: "false" + python-version: + description: "Python version to use." + required: false + default: "3.13" + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install reqstool (${{ inputs.reqstool-source }}) + shell: bash + run: | + if [ "${{ inputs.reqstool-source }}" = "main" ]; then + pip install "reqstool @ git+https://github.com/reqstool/reqstool-client.git@main" + else + pip install reqstool + fi + + - name: Run reqstool status + shell: bash + run: | + if [ "${{ inputs.fail-if-incomplete }}" = "true" ]; then + reqstool status --verbosity compact --check-all-reqs-met local -p "$GITHUB_WORKSPACE"/${{ inputs.reqstool-path }} + else + reqstool status --verbosity compact local -p "$GITHUB_WORKSPACE"/${{ inputs.reqstool-path }} + fi diff --git a/.github/actions/validate-reqstool/action.yml b/.github/actions/validate-reqstool/action.yml new file mode 100644 index 0000000..1b3a78d --- /dev/null +++ b/.github/actions/validate-reqstool/action.yml @@ -0,0 +1,37 @@ +name: "Validate reqstool" +description: "Install reqstool (from PyPI or reqstool-client@main) and run reqstool validate --strict (spec completeness: every requirement has SVCs, manual SVCs have MVRs) against a reqstool data path. Must run after the repo's own build step, in the same job, since it depends on build-time artifacts (e.g. annotations.yml, test results). The 'validate' subcommand isn't on PyPI yet — until it ships in a release, only invoke this action for a 'main' leg." + +inputs: + reqstool-source: + description: "Where to install reqstool from: 'pypi' (latest release) or 'main' (reqstool-client@main)." + required: false + default: "pypi" + reqstool-path: + description: "Path to the reqstool data directory (containing requirements.yml etc.), relative to repo root." + required: false + default: "docs/reqstool" + python-version: + description: "Python version to use." + required: false + default: "3.13" + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install reqstool (${{ inputs.reqstool-source }}) + shell: bash + run: | + if [ "${{ inputs.reqstool-source }}" = "main" ]; then + pip install "reqstool @ git+https://github.com/reqstool/reqstool-client.git@main" + else + pip install reqstool + fi + + - name: Run reqstool validate --strict + shell: bash + run: reqstool validate --strict local -p "$GITHUB_WORKSPACE"/${{ inputs.reqstool-path }} From af5b52af791ab1e8b9527a2642024febf11acf9e Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 21 Jun 2026 19:12:59 +0200 Subject: [PATCH 2/3] refactor(ci): extract shared install-reqstool composite action validate-reqstool and reqstool-status both duplicated the same "set up Python + pip install reqstool from pypi/main" logic. Extract it into a third composite action both now call via a nested `uses: ./.github/actions/install-reqstool`, so the install logic lives in one place. Found via self-review on #41. Signed-off-by: Jimisola Laursen --- .github/actions/install-reqstool/action.yml | 29 ++++++++++++++++++++ .github/actions/reqstool-status/action.yml | 14 ++-------- .github/actions/validate-reqstool/action.yml | 14 ++-------- 3 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 .github/actions/install-reqstool/action.yml diff --git a/.github/actions/install-reqstool/action.yml b/.github/actions/install-reqstool/action.yml new file mode 100644 index 0000000..4f8a6d9 --- /dev/null +++ b/.github/actions/install-reqstool/action.yml @@ -0,0 +1,29 @@ +name: "Install reqstool" +description: "Set up Python and install reqstool from either PyPI (latest release) or reqstool-client@main." + +inputs: + reqstool-source: + description: "Where to install reqstool from: 'pypi' (latest release) or 'main' (reqstool-client@main)." + required: false + default: "pypi" + python-version: + description: "Python version to use." + required: false + default: "3.13" + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install reqstool (${{ inputs.reqstool-source }}) + shell: bash + run: | + if [ "${{ inputs.reqstool-source }}" = "main" ]; then + pip install "reqstool @ git+https://github.com/reqstool/reqstool-client.git@main" + else + pip install reqstool + fi diff --git a/.github/actions/reqstool-status/action.yml b/.github/actions/reqstool-status/action.yml index bf6446d..5ddb780 100644 --- a/.github/actions/reqstool-status/action.yml +++ b/.github/actions/reqstool-status/action.yml @@ -22,20 +22,12 @@ inputs: runs: using: "composite" steps: - - name: Set up Python - uses: actions/setup-python@v6 + - name: Install reqstool + uses: ./.github/actions/install-reqstool with: + reqstool-source: ${{ inputs.reqstool-source }} python-version: ${{ inputs.python-version }} - - name: Install reqstool (${{ inputs.reqstool-source }}) - shell: bash - run: | - if [ "${{ inputs.reqstool-source }}" = "main" ]; then - pip install "reqstool @ git+https://github.com/reqstool/reqstool-client.git@main" - else - pip install reqstool - fi - - name: Run reqstool status shell: bash run: | diff --git a/.github/actions/validate-reqstool/action.yml b/.github/actions/validate-reqstool/action.yml index 1b3a78d..92b1f14 100644 --- a/.github/actions/validate-reqstool/action.yml +++ b/.github/actions/validate-reqstool/action.yml @@ -18,20 +18,12 @@ inputs: runs: using: "composite" steps: - - name: Set up Python - uses: actions/setup-python@v6 + - name: Install reqstool + uses: ./.github/actions/install-reqstool with: + reqstool-source: ${{ inputs.reqstool-source }} python-version: ${{ inputs.python-version }} - - name: Install reqstool (${{ inputs.reqstool-source }}) - shell: bash - run: | - if [ "${{ inputs.reqstool-source }}" = "main" ]; then - pip install "reqstool @ git+https://github.com/reqstool/reqstool-client.git@main" - else - pip install reqstool - fi - - name: Run reqstool validate --strict shell: bash run: reqstool validate --strict local -p "$GITHUB_WORKSPACE"/${{ inputs.reqstool-path }} From c580db95bea58cc6f1f958319149efbebfd0037e Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 21 Jun 2026 19:15:53 +0200 Subject: [PATCH 3/3] fix(security): pass action inputs via env instead of inline shell interpolation CodeQL flagged code-injection risk: ${{ inputs.x }} interpolated directly into run: shell blocks substitutes literal script text before execution, so a malicious input value could inject arbitrary shell commands. Pass all inputs used in run: blocks via env: instead and reference them as shell variables, which the shell expands safely without re-parsing as script. Affects install-reqstool, validate-reqstool, reqstool-status. Signed-off-by: Jimisola Laursen --- .github/actions/install-reqstool/action.yml | 6 ++++-- .github/actions/reqstool-status/action.yml | 9 ++++++--- .github/actions/validate-reqstool/action.yml | 4 +++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/actions/install-reqstool/action.yml b/.github/actions/install-reqstool/action.yml index 4f8a6d9..7d2a73e 100644 --- a/.github/actions/install-reqstool/action.yml +++ b/.github/actions/install-reqstool/action.yml @@ -19,10 +19,12 @@ runs: with: python-version: ${{ inputs.python-version }} - - name: Install reqstool (${{ inputs.reqstool-source }}) + - name: Install reqstool shell: bash + env: + REQSTOOL_SOURCE: ${{ inputs.reqstool-source }} run: | - if [ "${{ inputs.reqstool-source }}" = "main" ]; then + if [ "$REQSTOOL_SOURCE" = "main" ]; then pip install "reqstool @ git+https://github.com/reqstool/reqstool-client.git@main" else pip install reqstool diff --git a/.github/actions/reqstool-status/action.yml b/.github/actions/reqstool-status/action.yml index 5ddb780..c88e75b 100644 --- a/.github/actions/reqstool-status/action.yml +++ b/.github/actions/reqstool-status/action.yml @@ -30,9 +30,12 @@ runs: - name: Run reqstool status shell: bash + env: + FAIL_IF_INCOMPLETE: ${{ inputs.fail-if-incomplete }} + REQSTOOL_PATH: ${{ inputs.reqstool-path }} run: | - if [ "${{ inputs.fail-if-incomplete }}" = "true" ]; then - reqstool status --verbosity compact --check-all-reqs-met local -p "$GITHUB_WORKSPACE"/${{ inputs.reqstool-path }} + if [ "$FAIL_IF_INCOMPLETE" = "true" ]; then + reqstool status --verbosity compact --check-all-reqs-met local -p "$GITHUB_WORKSPACE/$REQSTOOL_PATH" else - reqstool status --verbosity compact local -p "$GITHUB_WORKSPACE"/${{ inputs.reqstool-path }} + reqstool status --verbosity compact local -p "$GITHUB_WORKSPACE/$REQSTOOL_PATH" fi diff --git a/.github/actions/validate-reqstool/action.yml b/.github/actions/validate-reqstool/action.yml index 92b1f14..216555d 100644 --- a/.github/actions/validate-reqstool/action.yml +++ b/.github/actions/validate-reqstool/action.yml @@ -26,4 +26,6 @@ runs: - name: Run reqstool validate --strict shell: bash - run: reqstool validate --strict local -p "$GITHUB_WORKSPACE"/${{ inputs.reqstool-path }} + env: + REQSTOOL_PATH: ${{ inputs.reqstool-path }} + run: reqstool validate --strict local -p "$GITHUB_WORKSPACE/$REQSTOOL_PATH"