Skip to content

ci(openspec): enforce archive-at-merge with a gate + /archive workflow#185

Open
gohabereg wants to merge 4 commits into
mainfrom
worktree-openspec-archive-ci
Open

ci(openspec): enforce archive-at-merge with a gate + /archive workflow#185
gohabereg wants to merge 4 commits into
mainfrom
worktree-openspec-archive-ci

Conversation

@gohabereg

Copy link
Copy Markdown
Member

What & why

Adds CI to keep openspec/specs/ in sync with merged changes, so a completed OpenSpec change can't merge without its delta specs being folded in. While building it I audited the existing .github/ workflows and folded in a round of hardening.

OpenSpec archiving

  • openspec-archive-gate — fails a PR / merge-queue build when a change has every task in tasks.md checked but is still sitting in openspec/changes/. Changes with unchecked tasks pass, so a multi-PR change only needs archiving in the PR that completes it. merge_group is the authoritative run; the openspec:defer-archive label softens the PR check only. Runs locally: node .github/scripts/openspec-archive-gate.mjs.
  • openspec-archive — a maintainer comments /archive (or /archive <change-name>). Runs the pinned openspec archive CLI, folds delta specs into openspec/specs/, and commits to the PR branch so the spec diff is reviewable and lands atomically with the code.
  • Pins @fission-ai/openspec as a devDependency; documents the flow in CONTRIBUTING.md and the delta-authoring rules in openspec/config.yaml.

Workflow hardening (existing CI)

  • Security — closed a command-injection vector where the PR comment body and changed-file names were interpolated directly into run: scripts; routed through env: instead. Added a write-access check on the commenter before /archive runs (a job that holds contents: write).
  • should-run-ci.sh — force CI when shared/root files change (lockfile, workflows, root manifests), so a package can't report "no CI needed" and merge green having tested nothing.
  • Robustness — concurrency guards on npm-publish and the docker image build; explicit least-privilege permissions: blocks; third-party actions (tj-actions/changed-files, softprops/turnstyle) pinned to full commit SHAs.
  • Cleanup — removed a dead github.ref != refs/heads/main test-skip guard, a redundant PR-number action, and a broken/unused output reference in aggregate-report.

Testing

  • All 26 workflow/action YAMLs parse.
  • openspec-archive-gate.mjs and should-run-ci.sh pass bash -n / node execution; should-run-ci.sh behavior verified across root-change, unrelated-change, package-change, and dependency-change scenarios.
  • Rebased cleanly onto the recent should-run-ci.sh workspace-resolution rewrite on main.

🤖 Generated with Claude Code

Add CI that keeps openspec/specs/ in sync with merged changes, plus a
round of hardening on the existing package-CI workflows found along the way.

OpenSpec archiving:
- openspec-archive-gate: fails a PR / merge-queue build when a change has
  every task checked but is still un-archived. merge_group is authoritative;
  the openspec:defer-archive label softens the PR check only.
- openspec-archive: maintainer comments `/archive` (or `/archive <name>`);
  runs the pinned openspec CLI, folds delta specs into openspec/specs/, and
  commits to the PR branch so the spec diff is reviewable.
- Pin @fission-ai/openspec as a devDependency; document the flow in
  CONTRIBUTING.md and the authoring rules in openspec/config.yaml.

Workflow hardening (existing CI):
- Fix a command-injection vector: PR comment body and changed-file names
  were spliced into run: scripts; route them through env instead, and
  verify commenter write-access before running /archive.
- should-run-ci.sh: force CI when shared/root files change (lockfile,
  workflows, root manifests) so a package can't skip CI having tested nothing.
- Add concurrency guards to npm-publish and docker-image; least-privilege
  permissions blocks; pin third-party actions (tj-actions/changed-files,
  softprops/turnstyle) to full SHAs.
- Remove a dead test-skip guard, a redundant PR-number action, and a
  broken/unused output reference in aggregate-report.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread .github/workflows/openspec-archive.yml Fixed
Comment thread .github/workflows/openspec-archive.yml Fixed
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Unit Tests

Package Coverage Delta
@editorjs/dom-adapters 86.95% 0% ⚪️
@editorjs/model 98.25% 0% ⚪️
@editorjs/ot-server 20% 0% ⚪️
@editorjs/core 90.47% 0% ⚪️
@editorjs/clipboard-plugin 66.66% 0% ⚪️
@editorjs/model-types 97.82% 0% ⚪️
@editorjs/collaboration-manager 85.81% 0% ⚪️

Mutation Tests

Package Mutation score Dashboard URL
@editorjs/dom-adapters No files to mutate found.
@editorjs/model No files to mutate found.
@editorjs/core No files to mutate found.
@editorjs/clipboard-plugin No files to mutate found.
@editorjs/collaboration-manager No files to mutate found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds CI enforcement and automation around the OpenSpec “archive at merge” workflow so that completed changes can’t merge without folding their delta specs into openspec/specs/. It also hardens existing GitHub Actions workflows against injection risks and improves CI correctness/robustness (root-change detection, pinned third-party actions, explicit permissions, and concurrency controls).

Changes:

  • Introduces an “OpenSpec archive gate” workflow and a Node gate script to block merges when a change is fully complete but not archived.
  • Adds a maintainer /archive issue-comment workflow that runs openspec archive and commits the folded specs + archived change back onto the PR branch.
  • Hardens existing workflows/actions (root-change CI forcing, action pinning, quoting/env routing, concurrency & permissions) and documents the new OpenSpec flow.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
package.json Pins @fission-ai/openspec as a devDependency for deterministic archiving in CI.
yarn.lock Adds the lock entries for @fission-ai/openspec@1.6.0 and its dependency tree.
openspec/config.yaml Documents delta-authoring constraints required for deterministic CLI folding.
CONTRIBUTING.md Updates contributor workflow docs to reflect “archive-at-merge-time” enforcement and /archive usage.
.github/workflows/openspec-archive-gate.yml Adds the PR/merge-queue gate workflow that runs the archive readiness check.
.github/scripts/openspec-archive-gate.mjs Implements the “completed tasks but not archived” detection logic used by the gate and /archive.
.github/workflows/openspec-archive.yml Adds the /archive issue-comment workflow that runs openspec archive and pushes a commit to the PR branch.
.github/scripts/should-run-ci.sh Forces CI when shared/root files change (lockfile, root manifests, workflows), preventing false “green” PRs.
.github/workflows/npm-publish.yml Adds least-privilege permissions and serializes publishes to avoid version-race conditions.
.github/workflows/build-and-push-docker-image.yml Adds explicit permissions and concurrency to prevent tag races on rapid merges.
.github/actions/unit-tests/action.yml Removes a main-branch coverage skip so coverage runs consistently.
.github/actions/mutation-tests-changed-files/action.yml Pins changed-files action and routes filenames via env to avoid shell injection.
.github/actions/mutation-tests-all-files/action.yml Pins changed-files and improves shell safety via env + quoting.
.github/actions/aggregate-report/action.yml Removes unused/broken output wiring and pins turnstyle; uses PR number directly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/openspec-archive.yml Outdated
Comment on lines +17 to +20
permissions:
contents: write
pull-requests: write

Comment on lines +84 to +96
const list = violations.map((c) => ` - ${c}`).join('\n');
const message =
`OpenSpec archive gate: the following change(s) are complete but not archived:\n${list}\n\n` +
`Run \`openspec archive -y <change>\` (or comment \`/archive\` on the PR) so the ` +
`delta specs fold into openspec/specs/ before this merges.`;

if (deferred) {
console.warn(`::warning::${message}\n(Deferred via openspec:defer-archive — not blocking.)`);
process.exit(0);
}

console.error(`::error::${message}`);
process.exit(1);
Comment on lines +61 to +62
run: |
echo "${{ steps.changed-files.outputs.src_all_changed_files }}" | sed 's/,/","/g' | sed 's/^/["/;s/$/"]/' > changed-files.json
printf '%s' "$CHANGED_FILES" | sed 's/,/","/g' | sed 's/^/["/;s/$/"]/' > changed-files.json
- openspec-archive.yml: split into archive-run (read-only token, runs the
  PR branch's code) and archive-commit (write token, runs no PR code and
  only applies the resulting patch), so the write token is never in scope
  while untrusted code executes — resolves the CodeQL untrusted-checkout
  TOCTOU alerts. Add issues: write for the comment-reaction endpoint.
- openspec-archive-gate.mjs: percent-encode ::warning::/::error:: messages
  so multi-line details aren't truncated in the annotation.
- mutation-tests-changed-files: build changed-files.json with jq instead of
  sed so filenames containing " or \ can't produce invalid JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread .github/workflows/openspec-archive.yml Fixed
CodeQL flagged archive-commit checking out the PR head (untrusted code) while
holding the write token ("checkout of untrusted code in a privileged context").

Build the archive commit in the unprivileged archive-run job and ship it as a
thin git bundle. archive-commit now checks out only the trusted default branch;
the untrusted objects arrive via `git fetch` (never a working-tree checkout,
never executed) and are pushed straight to the PR branch. A non-fast-forward
still fails rather than clobbering a moved branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread .github/workflows/openspec-archive.yml Dismissed
Comment thread .github/workflows/openspec-archive.yml Dismissed
archive-run executes the PR's own code (setup lifecycle scripts, openspec
archive) with a read-only token and ships the result as a git bundle;
archive-commit unbundles and pushes it with the write token, but never
validated that the bundle actually contained what archive-run was supposed
to produce.

Verify the bundled commit's parent matches the exact PR head archive-run
resolved, and that its diff only touches openspec/, before pushing. This is
real defense-in-depth against a compromised archive-run (e.g. a malicious
package.json script), and gives a concrete basis to review CodeQL alerts
#37/#38 (untrusted-checkout) against this job — it already follows GitHub's
documented mitigation of never executing PR code in the privileged job.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants