diff --git a/.github/workflows/refresh-decision-prepare.yml b/.github/workflows/refresh-decision-prepare.yml new file mode 100644 index 0000000..756e67c --- /dev/null +++ b/.github/workflows/refresh-decision-prepare.yml @@ -0,0 +1,136 @@ +name: Refresh Decision Prepare + +"on": + workflow_dispatch: + inputs: + package_ids: + description: "Comma-separated package IDs to compare." + required: true + default: "xyflow.workspace,xyflow.react,xyflow.svelte,xyflow.system" + package_id: + description: "Decision subject package ID." + required: true + default: "xyflow.workspace" + version: + description: "Package version to compare." + required: true + default: "0.1.0" + source_repository: + description: "Fresh upstream source repository URL." + required: true + default: "https://github.com/xyflow/xyflow" + source_revision: + description: "Fresh upstream source revision as a 40-character commit SHA." + required: true + default: "a58568f11bc0e1a1bdca1b3549e959e2e1ca0cdd" + fresh_generated_root: + description: "Fresh generated artifact root containing /." + required: true + default: "public-index/generated" + run_label: + description: "Human-readable label for the refresh evaluation." + required: true + default: "manual-refresh-decision-prepare" + +permissions: + contents: read + +jobs: + prepare-refresh-decision: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install SpecPM + run: python -m pip install -e ".[dev]" + + - name: Prepare refresh decision artifact + env: + PACKAGE_IDS: ${{ inputs.package_ids }} + PACKAGE_ID: ${{ inputs.package_id }} + VERSION: ${{ inputs.version }} + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + SOURCE_REVISION: ${{ inputs.source_revision }} + FRESH_GENERATED_ROOT: ${{ inputs.fresh_generated_root }} + RUN_LABEL: ${{ inputs.run_label }} + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/refresh-decision" + python - <<'PY' + import os + import subprocess + import sys + from pathlib import Path + + output_root = Path(os.environ["RUNNER_TEMP"]) / "refresh-decision" + decision_path = output_root / "refresh-decision.json" + report_path = output_root / "prepare-report.json" + package_ids = [ + value.strip() + for value in os.environ["PACKAGE_IDS"].replace("\n", ",").split(",") + if value.strip() + ] + if not package_ids: + raise SystemExit("package_ids input must contain at least one package ID") + + command = [ + sys.executable, + "-m", + "specpm.cli", + "producer-bundle", + "prepare-refresh-decision", + "--root", + os.environ["GITHUB_WORKSPACE"], + "--fresh-generated-root", + os.environ["FRESH_GENERATED_ROOT"], + "--package-id", + os.environ["PACKAGE_ID"], + "--version", + os.environ["VERSION"], + "--source-repository", + os.environ["SOURCE_REPOSITORY"], + "--source-revision", + os.environ["SOURCE_REVISION"], + "--run-label", + os.environ["RUN_LABEL"], + "--review-location", + f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}", + "--output", + str(decision_path), + "--json", + ] + for package_id in package_ids: + command.extend(["--package", package_id]) + + completed = subprocess.run(command, capture_output=True, text=True, check=False) + report_path.write_text(completed.stdout, encoding="utf-8") + if completed.stdout: + print(completed.stdout) + if completed.stderr: + print(completed.stderr, file=sys.stderr) + raise SystemExit(completed.returncode) + PY + + - name: Preflight prepared refresh decision + run: | + python -m specpm.cli producer-bundle preflight-refresh-decision \ + --body "$RUNNER_TEMP/refresh-decision/refresh-decision.json" \ + --root "$GITHUB_WORKSPACE" \ + --json \ + > "$RUNNER_TEMP/refresh-decision/preflight-report.json" + cat "$RUNNER_TEMP/refresh-decision/preflight-report.json" + + - name: Upload refresh decision artifact + uses: actions/upload-artifact@v7 + with: + name: generated-candidate-refresh-decision + path: | + ${{ runner.temp }}/refresh-decision/refresh-decision.json + ${{ runner.temp }}/refresh-decision/prepare-report.json + ${{ runner.temp }}/refresh-decision/preflight-report.json diff --git a/ROADMAP.md b/ROADMAP.md index 4c878a7..671172d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -540,6 +540,9 @@ tree with `specpm producer-bundle prepare-refresh-decision`, compare contract-bearing generated files, and emit read-only `SpecPMGeneratedCandidateRefreshDecisionPrepareReport` evidence before any maintainer registry decision. +The manual `Refresh Decision Prepare` GitHub Actions workflow now packages that +same refresh decision evidence as CI artifacts without write credentials or +registry mutation. SpecPM now has a consumer-side `preflight-ai-draft` gate for `SpecHarvesterPackageSetAIDraftProposal`. It verifies AI-proposed member selection, exclusions, and `contains` relations against deterministic workspace diff --git a/Sources/SpecPM/Documentation.docc/GeneratedCandidateRefreshDecisionPolicy.md b/Sources/SpecPM/Documentation.docc/GeneratedCandidateRefreshDecisionPolicy.md index 4d54d0a..c3e2e35 100644 --- a/Sources/SpecPM/Documentation.docc/GeneratedCandidateRefreshDecisionPolicy.md +++ b/Sources/SpecPM/Documentation.docc/GeneratedCandidateRefreshDecisionPolicy.md @@ -123,6 +123,20 @@ Matching contract files produce `status: no_update_required`, revision, or accepted-artifact drift produces `manual_review_required` with `updateNeeded: true`. Both outcomes remain review evidence only. +## CI Artifact + +The manual `.github/workflows/refresh-decision-prepare.yml` workflow runs the +same prepare helper through `workflow_dispatch` and uploads: + +- `refresh-decision.json` +- `prepare-report.json` +- `preflight-report.json` + +It defaults to the current `xyflow` package-set no-op comparison and exposes +inputs for package IDs, version, source repository, source revision, fresh +generated root, and run label. The workflow has `contents: read` permissions, +does not require write credentials, and does not mutate registry state. + ## Consumer-Side Preflight SpecPM can verify refresh decision records with: diff --git a/Sources/SpecPM/Documentation.docc/GitHubActionsPermissions.md b/Sources/SpecPM/Documentation.docc/GitHubActionsPermissions.md index d01a6f9..e4c6610 100644 --- a/Sources/SpecPM/Documentation.docc/GitHubActionsPermissions.md +++ b/Sources/SpecPM/Documentation.docc/GitHubActionsPermissions.md @@ -20,6 +20,9 @@ separate GitHub Pages deploy job with `pages: write` and `id-token: write`. The producer bundle preflight workflow is a read-only pull request check: it only runs consumer-side evidence validation when a pull request body includes producer bundle evidence blocks. +The refresh decision prepare workflow is also read-only: it runs only on manual +dispatch, prepares/preflights generated candidate refresh decision artifacts, +and uploads those artifacts for maintainer review without registry mutation. The only deployment secrets currently allowed are `FTP_HOST`, `FTP_PORT`, `FTP_USER`, `FTP_PASS`, and `FTP_REMOTE_ROOT`. They are scoped to the `FTP` diff --git a/Sources/SpecPM/Documentation.docc/ProducerBundleProposalPolicy.md b/Sources/SpecPM/Documentation.docc/ProducerBundleProposalPolicy.md index 96306d5..c77f2b1 100644 --- a/Sources/SpecPM/Documentation.docc/ProducerBundleProposalPolicy.md +++ b/Sources/SpecPM/Documentation.docc/ProducerBundleProposalPolicy.md @@ -219,6 +219,11 @@ consumer-side preflight checks. It is read-only review evidence and does not write accepted packages, generated candidates, relations, or public registry metadata. +The manual `.github/workflows/refresh-decision-prepare.yml` workflow wraps this +helper as a dry-run artifact producer. It runs with `contents: read`, uploads +the prepared decision plus prepare/preflight reports, and does not require +registry write credentials. + ## Fixture Alignment Cross-repository fixture ownership and drift handling are defined in diff --git a/Sources/SpecPM/Documentation.docc/Roadmap.md b/Sources/SpecPM/Documentation.docc/Roadmap.md index acc4f1c..abcf058 100644 --- a/Sources/SpecPM/Documentation.docc/Roadmap.md +++ b/Sources/SpecPM/Documentation.docc/Roadmap.md @@ -315,6 +315,9 @@ tree with `specpm producer-bundle prepare-refresh-decision`, compare contract-bearing generated files, and emit read-only `SpecPMGeneratedCandidateRefreshDecisionPrepareReport` evidence before any maintainer registry decision. +The manual `Refresh Decision Prepare` GitHub Actions workflow now packages that +same refresh decision evidence as CI artifacts without write credentials or +registry mutation. SpecPM now has a consumer-side `preflight-ai-draft` gate for `SpecHarvesterPackageSetAIDraftProposal`. It verifies AI-proposed member selection, exclusions, and `contains` relations against deterministic workspace diff --git a/specpm.yaml b/specpm.yaml index ea64027..a732c5a 100644 --- a/specpm.yaml +++ b/specpm.yaml @@ -34,6 +34,7 @@ index: - specpm.registry.generated_candidate_refresh_decision_policy - specpm.registry.generated_candidate_refresh_decision_preflight - specpm.registry.generated_candidate_refresh_decision_prepare + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - specpm.registry.index_removal_request_flow - specpm.registry.namespace_claim_request_flow - specpm.registry.namespace_claim_policy diff --git a/specs/GENERATED_CANDIDATE_REFRESH_DECISION_POLICY.md b/specs/GENERATED_CANDIDATE_REFRESH_DECISION_POLICY.md index c0c49aa..cb4267b 100644 --- a/specs/GENERATED_CANDIDATE_REFRESH_DECISION_POLICY.md +++ b/specs/GENERATED_CANDIDATE_REFRESH_DECISION_POLICY.md @@ -256,6 +256,22 @@ artifact assumptions differ, the draft uses `manual_review_required` and `updateNeeded: true`; it still does not mutate curated artifacts, generated artifacts, relations, or registry metadata. +## CI Artifact + +The manual `.github/workflows/refresh-decision-prepare.yml` workflow runs the +same prepare helper through `workflow_dispatch` and uploads three review +artifacts: + +- `refresh-decision.json` +- `prepare-report.json` +- `preflight-report.json` + +The workflow defaults to the current `xyflow` package-set no-op comparison, but +maintainers can override package IDs, version, source repository, source +revision, fresh generated root, and run label. It has `contents: read` +permissions, does not require write credentials, and does not mutate accepted +packages, generated candidates, accepted relations, or registry metadata. + ## Consumer-Side Preflight SpecPM can verify a generated candidate refresh decision before maintainers use diff --git a/specs/GITHUB_ACTIONS_PERMISSIONS.md b/specs/GITHUB_ACTIONS_PERMISSIONS.md index 3c95014..6d9fd57 100644 --- a/specs/GITHUB_ACTIONS_PERMISSIONS.md +++ b/specs/GITHUB_ACTIONS_PERMISSIONS.md @@ -47,6 +47,7 @@ all secrets as privileged and keeps explicit workflow boundaries around them. | `.github/workflows/package-submission-check.yml` | `issues` labeled `package-submission` | `contents: read`, `issues: write`. | No repository or environment secrets. | Validate public package submission text and post/update one validation comment. | | `.github/workflows/package-submission-triage.yml` | `issues` labeled `package-submission` | `contents: read`, `issues: write`. | No repository or environment secrets. | Prepare review labels and an idempotent maintainer guidance comment. | | `.github/workflows/producer-bundle-preflight.yml` | `pull_request` | `contents: read`. | No repository or environment secrets. | If the pull request body includes producer bundle evidence blocks, run consumer-side `specpm producer-bundle preflight`; otherwise report a skip. | +| `.github/workflows/refresh-decision-prepare.yml` | `workflow_dispatch` | `contents: read`. | No repository or environment secrets. | Prepare and preflight generated candidate refresh decision artifacts for maintainer review. | | `.github/workflows/namespace-claim-triage.yml` | `issues` labeled `namespace-claim` | `contents: read`, `issues: write`. | No repository or environment secrets. | Prepare namespace review labels and an idempotent policy comment. | | `.github/workflows/namespace-claim-decision-report.yml` | `issues` labeled/unlabeled `namespace-claim` | `contents: read`, `issues: write`. | No repository or environment secrets. | Report maintainer-applied namespace decision labels. | | `.github/workflows/namespace-claim-decision-summary.yml` | `workflow_dispatch`, schedule | `contents: read`, `issues: read`. | No repository or environment secrets. | Write read-only namespace decision summaries and upload workflow artifacts. | @@ -68,10 +69,12 @@ by: - `.github/workflows/deploy-connection-check.yml`, job `deploy-connection-check`. -Package submission, producer bundle preflight, and namespace claim workflows -must not read repository or environment secrets. They process public GitHub -Issue or pull request content and may use `GITHUB_TOKEN` only for the documented -comment, label, search, artifact, and read-only checkout operations. +Package submission, producer bundle preflight, refresh decision prepare, and +namespace claim workflows must not read repository or environment secrets. They +process public GitHub Issue or pull request content, checked-in generated +artifacts, or maintainer-provided workflow inputs, and may use `GITHUB_TOKEN` +only for the documented comment, label, search, artifact, and read-only checkout +operations. ## `pull_request_target` Review Rules diff --git a/specs/MULTI_PACKAGE_PRODUCER_INTAKE.md b/specs/MULTI_PACKAGE_PRODUCER_INTAKE.md index 1773579..c7d50fc 100644 --- a/specs/MULTI_PACKAGE_PRODUCER_INTAKE.md +++ b/specs/MULTI_PACKAGE_PRODUCER_INTAKE.md @@ -261,6 +261,8 @@ When a fresh package-set run is available, maintainers can use from generated contract-file comparisons before running `preflight-refresh-decision`. This remains evidence preparation, not package or relation acceptance. +For a GitHub Actions artifact trail, the manual `Refresh Decision Prepare` +workflow can upload the prepared decision and prepare/preflight reports. ## Bundle-Set Checklist diff --git a/specs/PRODUCER_BUNDLE_PROPOSAL_POLICY.md b/specs/PRODUCER_BUNDLE_PROPOSAL_POLICY.md index 4e2a8b6..c7a1d17 100644 --- a/specs/PRODUCER_BUNDLE_PROPOSAL_POLICY.md +++ b/specs/PRODUCER_BUNDLE_PROPOSAL_POLICY.md @@ -314,6 +314,11 @@ consumer-side preflight checks. It is read-only review evidence and does not write accepted packages, generated candidates, relations, or public registry metadata. +The manual `.github/workflows/refresh-decision-prepare.yml` workflow wraps this +helper as a dry-run artifact producer. It runs with `contents: read`, uploads +the prepared decision plus prepare/preflight reports, and does not require +registry write credentials. + ## Fixture Alignment Cross-repository fixture ownership and drift handling are defined in diff --git a/specs/PUBLIC_INDEX_OPERATOR_GUIDE.md b/specs/PUBLIC_INDEX_OPERATOR_GUIDE.md index 6b76880..de8c9db 100644 --- a/specs/PUBLIC_INDEX_OPERATOR_GUIDE.md +++ b/specs/PUBLIC_INDEX_OPERATOR_GUIDE.md @@ -210,6 +210,11 @@ files, emits `SpecPMGeneratedCandidateRefreshDecisionPrepareReport`, writes the draft decision with `--output`, and performs the same preflight checks. It is read-only review evidence. +For an auditable GitHub Actions run, use the manual +`Refresh Decision Prepare` workflow. It uploads `refresh-decision.json`, +`prepare-report.json`, and `preflight-report.json` as review artifacts with +read-only repository permissions. + When a `SpecPMGeneratedCandidateRefreshDecision` record is available, maintainers should run: diff --git a/specs/WORKPLAN.md b/specs/WORKPLAN.md index 856ee6f..02a2aee 100644 --- a/specs/WORKPLAN.md +++ b/specs/WORKPLAN.md @@ -2410,6 +2410,45 @@ Result: CLI reference, DocC, roadmap, and self-spec document the helper and non-authority boundary. +### P66-T21. Generated Candidate Refresh Decision CI Artifact + +Status: Completed. + +Motivation: + +- P66-T20 gives maintainers a local prepare helper, but review should also have + an auditable GitHub Actions path that produces the same machine-readable + evidence. +- CI artifact generation should not require registry write credentials or look + like package acceptance. + +Goal: + +- Add a manual `Refresh Decision Prepare` workflow that runs + `specpm producer-bundle prepare-refresh-decision`, then runs + `preflight-refresh-decision` against the prepared output. +- Upload `refresh-decision.json`, `prepare-report.json`, and + `preflight-report.json` as workflow artifacts. +- Keep workflow permissions read-only and default inputs aligned with the real + `xyflow` package-set no-op comparison. + +Expected result: + +- Maintainers can run a dry-run refresh evaluation from GitHub Actions and + attach the resulting artifact bundle to review. +- The workflow is explicit `workflow_dispatch`, has `contents: read`, and does + not mutate accepted packages, generated candidates, accepted relations, or + public registry metadata. + +Result: + +- `.github/workflows/refresh-decision-prepare.yml` prepares and preflights the + refresh decision artifact bundle. +- Policy docs, operator guide, DocC, roadmap, and self-spec document that the + workflow is evidence-only and credential-free. +- Regression coverage parses the workflow and checks manual dispatch inputs, + read-only permissions, prepare/preflight commands, and artifact upload paths. + ## Post-MVP Tracks - Remote registry service implementation. diff --git a/specs/specpm.spec.yaml b/specs/specpm.spec.yaml index b2fa992..05a53cb 100644 --- a/specs/specpm.spec.yaml +++ b/specs/specpm.spec.yaml @@ -178,6 +178,9 @@ provides: - id: specpm.registry.generated_candidate_refresh_decision_prepare role: secondary summary: Prepare draft SpecPMGeneratedCandidateRefreshDecision records from fresh/current generated artifact comparisons without mutating registry state. + - id: specpm.registry.generated_candidate_refresh_decision_ci_artifact + role: secondary + summary: Produce read-only GitHub Actions refresh decision artifacts for maintainer review without registry write credentials. - id: specpm.registry.package_set_relation_metadata role: secondary summary: Publish maintainer-reviewed package-set relation metadata through additive `/v0` payloads and the static viewer without inferring relations from producer output. @@ -742,6 +745,7 @@ evidence: - specpm.registry.public_index_accepted_manifest - specpm.registry.generated_candidate_refresh_decision_preflight - specpm.registry.generated_candidate_refresh_decision_prepare + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - id: add_specpackages_issue_template kind: documentation path: .github/ISSUE_TEMPLATE/add-specpackages.yml @@ -837,6 +841,13 @@ evidence: path: .github/workflows/producer-bundle-preflight.yml supports: - specpm.specs.producer_bundle_proposal_policy + - id: refresh_decision_prepare_workflow + kind: source + path: .github/workflows/refresh-decision-prepare.yml + supports: + - specpm.registry.generated_candidate_refresh_decision_ci_artifact + - specpm.registry.generated_candidate_refresh_decision_prepare + - specpm.registry.generated_candidate_refresh_decision_preflight - id: producer_bundle_source kind: source path: src/specpm/producer_bundle.py @@ -969,6 +980,7 @@ evidence: path: specs/GITHUB_ACTIONS_PERMISSIONS.md supports: - specpm.deployment.github_actions_permissions_policy + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - github_actions_least_privilege - scope.includes - scope.excludes @@ -1020,6 +1032,7 @@ evidence: - specpm.specs.ai_draft_preflight - specpm.registry.generated_candidate_refresh_decision_preflight - specpm.registry.generated_candidate_refresh_decision_prepare + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - specpm.registry.package_set_materialization_helper - specpm.specs.producer_receipt_contract - producer_receipts_not_generation_authority @@ -1174,6 +1187,7 @@ evidence: - specpm.registry.generated_candidate_refresh_decision_policy - specpm.registry.generated_candidate_refresh_decision_preflight - specpm.registry.generated_candidate_refresh_decision_prepare + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - specpm.registry.curated_artifact_lifecycle - specpm.registry.curated_accepted_artifacts - specpm.specs.multi_package_producer_intake @@ -1311,6 +1325,7 @@ evidence: - specpm.specs.ai_draft_preflight - specpm.registry.generated_candidate_refresh_decision_preflight - specpm.registry.generated_candidate_refresh_decision_prepare + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - id: core_source kind: source path: src/specpm/core.py @@ -1440,6 +1455,7 @@ evidence: supports: - specpm.documentation.docc_site - specpm.deployment.github_actions_permissions_policy + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - id: docc_remote_package_acquisition kind: documentation path: Sources/SpecPM/Documentation.docc/RemotePackageAcquisition.md @@ -1599,6 +1615,7 @@ evidence: - specpm.registry.generated_candidate_refresh_decision_policy - specpm.registry.generated_candidate_refresh_decision_preflight - specpm.registry.generated_candidate_refresh_decision_prepare + - specpm.registry.generated_candidate_refresh_decision_ci_artifact - specpm.registry.curated_artifact_lifecycle - specpm.registry.curated_accepted_artifacts - specpm.specs.multi_package_producer_intake diff --git a/tests/test_core.py b/tests/test_core.py index f080159..0d5121c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -80,6 +80,7 @@ PACKAGE_SUBMISSION_WORKFLOW = ROOT / ".github/workflows/package-submission-check.yml" PACKAGE_SUBMISSION_TRIAGE_WORKFLOW = ROOT / ".github/workflows/package-submission-triage.yml" PRODUCER_BUNDLE_PREFLIGHT_WORKFLOW = ROOT / ".github/workflows/producer-bundle-preflight.yml" +REFRESH_DECISION_PREPARE_WORKFLOW = ROOT / ".github/workflows/refresh-decision-prepare.yml" NAMESPACE_CLAIM_TRIAGE_WORKFLOW = ROOT / ".github/workflows/namespace-claim-triage.yml" NAMESPACE_CLAIM_DECISION_REPORT_WORKFLOW = ( ROOT / ".github/workflows/namespace-claim-decision-report.yml" @@ -1902,6 +1903,52 @@ def test_package_submission_triage_workflow_applies_review_labels_only() -> None assert forbidden not in script +def test_refresh_decision_prepare_workflow_uploads_read_only_artifacts() -> None: + loaded = load_yaml_file(REFRESH_DECISION_PREPARE_WORKFLOW) + + assert loaded["name"] == "Refresh Decision Prepare" + assert loaded["permissions"] == {"contents": "read"} + assert "workflow_dispatch" in loaded["on"] + inputs = loaded["on"]["workflow_dispatch"]["inputs"] + assert inputs["package_ids"]["default"] == ( + "xyflow.workspace,xyflow.react,xyflow.svelte,xyflow.system" + ) + assert inputs["package_id"]["default"] == "xyflow.workspace" + assert inputs["version"]["default"] == "0.1.0" + assert inputs["source_repository"]["default"] == "https://github.com/xyflow/xyflow" + assert inputs["source_revision"]["default"] == "a58568f11bc0e1a1bdca1b3549e959e2e1ca0cdd" + assert inputs["fresh_generated_root"]["default"] == "public-index/generated" + + job = loaded["jobs"]["prepare-refresh-decision"] + steps = {step["name"]: step for step in job["steps"] if "name" in step} + assert steps["Check out repository"]["uses"] == "actions/checkout@v6" + assert steps["Set up Python"]["uses"] == "actions/setup-python@v6" + prepare_script = steps["Prepare refresh decision artifact"]["run"] + assert "prepare-refresh-decision" in prepare_script + assert "--output" in prepare_script + assert "refresh-decision.json" in prepare_script + assert "prepare-report.json" in prepare_script + preflight_script = steps["Preflight prepared refresh decision"]["run"] + assert "preflight-refresh-decision" in preflight_script + assert "preflight-report.json" in preflight_script + upload_step = steps["Upload refresh decision artifact"] + assert upload_step["uses"] == "actions/upload-artifact@v7" + assert upload_step["with"]["name"] == "generated-candidate-refresh-decision" + assert "refresh-decision.json" in upload_step["with"]["path"] + assert "prepare-report.json" in upload_step["with"]["path"] + assert "preflight-report.json" in upload_step["with"]["path"] + + workflow_text = REFRESH_DECISION_PREPARE_WORKFLOW.read_text(encoding="utf-8") + for forbidden in ( + "git push", + "gh pr create", + "public-index generate", + "FTP_PASS", + "contents: write", + ): + assert forbidden not in workflow_text + + def test_namespace_claim_triage_workflow_applies_review_labels_only() -> None: loaded = load_yaml_file(NAMESPACE_CLAIM_TRIAGE_WORKFLOW) @@ -2151,6 +2198,7 @@ def test_github_actions_permissions_and_secrets_boundary_is_documented() -> None PACKAGE_SUBMISSION_WORKFLOW: {"contents": "read", "issues": "write"}, PACKAGE_SUBMISSION_TRIAGE_WORKFLOW: {"contents": "read", "issues": "write"}, PRODUCER_BUNDLE_PREFLIGHT_WORKFLOW: {"contents": "read"}, + REFRESH_DECISION_PREPARE_WORKFLOW: {"contents": "read"}, NAMESPACE_CLAIM_TRIAGE_WORKFLOW: {"contents": "read", "issues": "write"}, NAMESPACE_CLAIM_DECISION_REPORT_WORKFLOW: {"contents": "read", "issues": "write"}, NAMESPACE_CLAIM_DECISION_SUMMARY_WORKFLOW: {"contents": "read", "issues": "read"}, @@ -3349,6 +3397,10 @@ def test_generated_candidate_refresh_decision_policy_is_documented() -> None: "fresh real `xyflow` package-set run", "specpm producer-bundle prepare-refresh-decision", "SpecPMGeneratedCandidateRefreshDecisionPrepareReport", + ".github/workflows/refresh-decision-prepare.yml", + "refresh-decision.json", + "prepare-report.json", + "preflight-report.json", "specpm producer-bundle preflight-refresh-decision", "SpecPMGeneratedCandidateRefreshDecisionPreflightReport", "status/update consistency", @@ -3367,6 +3419,10 @@ def test_generated_candidate_refresh_decision_policy_is_documented() -> None: "producer receipt churn plus an additional advisory quality report", "specpm producer-bundle prepare-refresh-decision", "SpecPMGeneratedCandidateRefreshDecisionPrepareReport", + ".github/workflows/refresh-decision-prepare.yml", + "refresh-decision.json", + "prepare-report.json", + "preflight-report.json", "specpm producer-bundle preflight-refresh-decision", "SpecPMGeneratedCandidateRefreshDecisionPreflightReport", "status/update consistency", @@ -3391,6 +3447,7 @@ def test_generated_candidate_refresh_decision_policy_is_documented() -> None: "`SpecPMGeneratedCandidateRefreshDecision` with `updateNeeded: false`", "`reason: no_contract_delta`", "specpm producer-bundle prepare-refresh-decision", + "Refresh Decision Prepare", ): assert required_text in intake_flat assert required_text in operator_guide_flat @@ -3410,6 +3467,7 @@ def test_generated_candidate_refresh_decision_policy_is_documented() -> None: "`reason: no_contract_delta`", "curated accepted artifact remains the stronger registry source", "SpecPMGeneratedCandidateRefreshDecisionPrepareReport", + "Refresh Decision Prepare", ): assert required_text in roadmap_flat assert required_text in docc_roadmap_flat @@ -3434,15 +3492,22 @@ def test_generated_candidate_refresh_decision_policy_is_documented() -> None: "P66-T20. Generated Candidate Refresh Decision Prepare Helper", "`specpm producer-bundle prepare-refresh-decision`", "`SpecPMGeneratedCandidateRefreshDecisionPrepareReport`", + "P66-T21. Generated Candidate Refresh Decision CI Artifact", + "`refresh-decision.json`", + "`prepare-report.json`", + "`preflight-report.json`", ): assert required_text in workplan_flat assert "specpm.registry.generated_candidate_refresh_decision_policy" in manifest assert "specpm.registry.generated_candidate_refresh_decision_preflight" in manifest assert "specpm.registry.generated_candidate_refresh_decision_prepare" in manifest + assert "specpm.registry.generated_candidate_refresh_decision_ci_artifact" in manifest assert "specpm.registry.generated_candidate_refresh_decision_policy" in self_spec assert "specpm.registry.generated_candidate_refresh_decision_preflight" in self_spec assert "specpm.registry.generated_candidate_refresh_decision_prepare" in self_spec + assert "specpm.registry.generated_candidate_refresh_decision_ci_artifact" in self_spec + assert ".github/workflows/refresh-decision-prepare.yml" in self_spec assert "specs/GENERATED_CANDIDATE_REFRESH_DECISION_POLICY.md" in self_spec assert ( "Sources/SpecPM/Documentation.docc/GeneratedCandidateRefreshDecisionPolicy.md" in self_spec