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
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* @recodeecom
# Default code owners for repository-wide reviews.
* @recodeecom @NagyVikt
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
versioning-strategy: lockfile-only
- package-ecosystem: github-actions
directory: /
schedule:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '35 3 * * 1'

Expand Down
37 changes: 36 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Release to npm (provenance)
name: Release to npm (provenance + signed assets)

on:
workflow_dispatch:
Expand All @@ -14,6 +14,9 @@ jobs:
if: github.repository == 'recodeee/gitguardex'
runs-on: ubuntu-latest
environment: npm
permissions:
contents: write
id-token: write

steps:
- name: Checkout
Expand All @@ -28,6 +31,9 @@ jobs:
registry-url: https://registry.npmjs.org
cache: npm

- name: Install Cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1

- name: Install
run: npm ci --ignore-scripts

Expand Down Expand Up @@ -55,6 +61,14 @@ jobs:
echo "already_published=false" >> "$GITHUB_OUTPUT"
fi

- name: Pack release tarball
id: pack
run: |
mkdir -p dist
package_file="$(npm pack --pack-destination dist | tail -n 1)"
echo "package_file=${package_file}" >> "$GITHUB_OUTPUT"
echo "package_path=dist/${package_file}" >> "$GITHUB_OUTPUT"

- name: Publish with provenance
if: ${{ steps.registry.outputs.already_published != 'true' }}
run: npm publish --provenance --access public
Expand All @@ -65,3 +79,24 @@ jobs:
PACKAGE_NAME: ${{ steps.pkg.outputs.name }}
PACKAGE_VERSION: ${{ steps.pkg.outputs.version }}
run: echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already on npm; skipping publish."

- name: Generate release checksum
run: |
sha256sum "${{ steps.pack.outputs.package_path }}" > "${{ steps.pack.outputs.package_path }}.sha256"

- name: Sign release tarball with Sigstore
run: |
cosign sign-blob "${{ steps.pack.outputs.package_path }}" \
--bundle "${{ steps.pack.outputs.package_path }}.sigstore.json" \
--yes

- name: Upload signed release assets
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.release.tag_name || format('v{0}', steps.pkg.outputs.version) }}
run: |
gh release upload "$RELEASE_TAG" \
"${{ steps.pack.outputs.package_path }}" \
"${{ steps.pack.outputs.package_path }}.sha256" \
"${{ steps.pack.outputs.package_path }}.sigstore.json" \
--clobber
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Only the latest published GitGuardex CLI build is supported for security fixes.

Please report security issues privately by opening a GitHub security advisory:

- https://github.com/recodeecom/multiagent-safety/security/advisories/new
- https://github.com/recodeee/gitguardex/security/advisories/new

If advisories are unavailable, open a private report via GitHub issue contact details and avoid posting exploit details publicly.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-23
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Why

- GitGuardex already ships several Scorecard-friendly guardrails, but key supply-chain signals still lag behind the repository's actual intent.
- The release workflow publishes to npm with provenance but leaves GitHub releases without signed assets, so Scorecard cannot award signed-release credit.
- CI and CodeQL do not run on pull requests to `main`, which weakens branch-protection hardening because there are no stable pre-merge checks to require.
- Package metadata and security/reporting metadata still contain avoidable drift such as range-based dependency specifiers and a repo link that points elsewhere.

## What Changes

- Run CI and CodeQL for pull requests targeting `main` so required status checks have real pre-merge coverage.
- Extend the release workflow to build the npm tarball, checksum it, sign it with a Sigstore bundle, and upload those artifacts to the matching GitHub release.
- Expand Dependabot to cover npm packages and pin package dependency specifiers exactly in both `package.json` and `package-lock.json`.
- Correct security-reporting and code-owner metadata needed for stricter GitHub review settings.

## Impact

- Affects GitHub Actions workflows, dependency metadata, and security/review docs only; runtime CLI behavior stays unchanged.
- Signed-release score gains apply to future or re-run releases; historical releases without assets remain unchanged until republished or manually backfilled.
- Branch-protection, maintained, contributors, and code-review scores still depend partly on live GitHub settings, repo age, and human review history outside this diff.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## ADDED Requirements

### Requirement: Pull request checks for protected main
The repository SHALL run CI and CodeQL workflows on pull requests targeting `main` so branch-protection rules can require real pre-merge status checks.

#### Scenario: CI runs on pull requests
- **WHEN** a pull request targets `main`
- **THEN** `.github/workflows/ci.yml` triggers for that pull request
- **AND** the workflow remains enabled for direct pushes to `main`.

#### Scenario: CodeQL runs on pull requests
- **WHEN** a pull request targets `main`
- **THEN** `.github/workflows/codeql.yml` triggers for that pull request
- **AND** the scheduled scan remains enabled.

### Requirement: Signed GitHub release assets
The release workflow SHALL publish signed GitHub release assets for the package tarball in addition to npm provenance.

#### Scenario: Release uploads signed artifacts
- **WHEN** `.github/workflows/release.yml` runs for a published release
- **THEN** it builds the npm tarball, generates a SHA256 checksum, creates a Sigstore bundle for the tarball, and uploads those files to the matching GitHub release
- **AND** the workflow continues to publish to npm with provenance when the version is not already published.

### Requirement: Pinned dependency and update metadata
The repository SHALL keep supply-chain metadata aligned with stricter Scorecard expectations.

#### Scenario: Package specs stay exact
- **WHEN** runtime or dev dependencies are declared in `package.json`
- **THEN** their versions are pinned exactly
- **AND** `package-lock.json` reflects those exact specifiers.

#### Scenario: Automated update coverage includes npm
- **WHEN** Dependabot configuration is evaluated
- **THEN** it schedules updates for both npm dependencies and GitHub Actions.

### Requirement: Security and ownership metadata points at this repository
Repository security and ownership metadata SHALL reference the live GitGuardex repository surfaces.

#### Scenario: Security reporting points at this repo
- **WHEN** maintainers or users read `SECURITY.md`
- **THEN** the private advisory link targets `recodeee/gitguardex`.

#### Scenario: Code owners cover default review paths
- **WHEN** repository-wide ownership is evaluated
- **THEN** `.github/CODEOWNERS` defines default owners for all files.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## Definition of Done

This change is complete only when **all** of the following are true:

- Every checkbox below is checked.
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.

## Handoff

- Handoff: change=`agent-codex-harden-scorecard-best-practices-2026-04-23-18-42`; branch=`agent/codex/harden-scorecard-best-practices-2026-04-23-18-42`; scope=`Scorecard-facing workflows, dependency metadata, and security/review docs`; action=`finish repo-side hardening, then apply and verify reachable GitHub settings`.
- Copy prompt: Continue `agent-codex-harden-scorecard-best-practices-2026-04-23-18-42` on branch `agent/codex/harden-scorecard-best-practices-2026-04-23-18-42`. Work inside the existing sandbox, review `openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/harden-scorecard-best-practices-2026-04-23-18-42 --base main --via-pr --wait-for-merge --cleanup`.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-harden-scorecard-best-practices-2026-04-23-18-42`.
- [x] 1.2 Define normative requirements in `specs/harden-scorecard-best-practices/spec.md`.

## 2. Implementation

- [x] 2.1 Implement scoped behavior changes.
- [x] 2.2 Add/update focused regression coverage.

## 3. Verification

- [x] 3.1 Run targeted project verification commands.
- [x] 3.2 Run `openspec validate agent-codex-harden-scorecard-best-practices-2026-04-23-18-42 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

## 4. Cleanup (mandatory; run before claiming completion)

- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/harden-scorecard-best-practices-2026-04-23-18-42 --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@
"access": "public"
},
"devDependencies": {
"fast-check": "^3.23.2"
"fast-check": "3.23.2"
},
"dependencies": {
"jsonc-parser": "^3.3.1",
"semver": "^7.7.4"
"jsonc-parser": "3.3.1",
"semver": "7.7.4"
}
}
50 changes: 50 additions & 0 deletions test/metadata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ test('release workflow publishes with provenance in CI', () => {
const workflow = fs.readFileSync(workflowPath, 'utf8');
assert.match(workflow, /name:\s+Checkout\s+uses:\s+actions\/checkout@[0-9a-f]{40}[^\n]*\n\s+with:\s*\n\s+fetch-depth:\s+0/s);
assert.match(workflow, /npm publish --provenance --access public/);
assert.match(workflow, /name:\s+Install Cosign\s+uses:\s+sigstore\/cosign-installer@[0-9a-f]{40}[^\n]*# v4\.1\.1/s);
});

test('release workflow skips publish when the current version is already on npm', () => {
Expand All @@ -45,6 +46,19 @@ test('release workflow skips publish when the current version is already on npm'
assert.match(workflow, /skipping publish\./);
});

test('release workflow uploads signed GitHub release assets for the package tarball', () => {
const workflowPath = path.join(repoRoot, '.github', 'workflows', 'release.yml');
const workflow = fs.readFileSync(workflowPath, 'utf8');
assert.match(workflow, /name:\s+Pack release tarball/);
assert.match(workflow, /npm pack --pack-destination dist/);
assert.match(workflow, /sha256sum "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}" > "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}\.sha256"/);
assert.match(workflow, /cosign sign-blob "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}"/);
assert.match(workflow, /--bundle "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}\.sigstore\.json"/);
assert.match(workflow, /name:\s+Upload signed release assets/);
assert.match(workflow, /GH_TOKEN:\s+\$\{\{\s*github\.token\s*\}\}/);
assert.match(workflow, /gh release upload "\$RELEASE_TAG"/);
});

test('release workflow only publishes from published releases or manual dispatch', () => {
const workflowPath = path.join(repoRoot, '.github', 'workflows', 'release.yml');
const workflow = fs.readFileSync(workflowPath, 'utf8');
Expand Down Expand Up @@ -129,6 +143,13 @@ test('security workflows are present and use pinned GitHub Actions SHAs', () =>
}
});

test('CI and CodeQL workflows run on pull requests targeting main', () => {
const ciWorkflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8');
const codeqlWorkflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'codeql.yml'), 'utf8');
assert.match(ciWorkflow, /pull_request:\s*\n\s*branches:\s*\n\s*-\s*main/s);
assert.match(codeqlWorkflow, /pull_request:\s*\n\s*branches:\s*\n\s*-\s*main/s);
});

test('code review workflow does not gate startup on secrets context', () => {
const workflowPath = path.join(repoRoot, '.github', 'workflows', 'cr.yml');
const workflow = fs.readFileSync(workflowPath, 'utf8');
Expand All @@ -137,6 +158,35 @@ test('code review workflow does not gate startup on secrets context', () => {
assert.match(workflow, /if:\s+\$\{\{\s*env\.OPENAI_API_KEY != ''\s*\}\}/);
});

test('security metadata points at the live repo and dependabot covers npm plus actions', () => {
const securityPolicy = fs.readFileSync(path.join(repoRoot, 'SECURITY.md'), 'utf8');
const dependabot = fs.readFileSync(path.join(repoRoot, '.github', 'dependabot.yml'), 'utf8');
const codeowners = fs.readFileSync(path.join(repoRoot, '.github', 'CODEOWNERS'), 'utf8');

assert.match(securityPolicy, /https:\/\/github\.com\/recodeee\/gitguardex\/security\/advisories\/new/);
assert.match(dependabot, /package-ecosystem:\s+npm/);
assert.match(dependabot, /package-ecosystem:\s+github-actions/);
assert.match(dependabot, /versioning-strategy:\s+lockfile-only/);
assert.match(codeowners, /^# Default code owners/m);
assert.match(codeowners, /^\*\s+@recodeecom\s+@NagyVikt$/m);
});

test('package manifest pins runtime and dev dependency versions exactly', () => {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const lockfile = fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8');

for (const sectionName of ['dependencies', 'devDependencies']) {
const section = pkg[sectionName] || {};
for (const [name, version] of Object.entries(section)) {
assert.doesNotMatch(version, /^[~^]/, `${sectionName}.${name} must stay pinned exactly`);
}
}

assert.match(lockfile, /"jsonc-parser": "3\.3\.1"/);
assert.match(lockfile, /"semver": "7\.7\.4"/);
assert.match(lockfile, /"fast-check": "3\.23\.2"/);
});

test('frontend mirror workflow skips cleanly when the mirror PAT is missing', () => {
const workflowPath = path.join(repoRoot, '.github', 'workflows', 'sync-frontend-mirror.yml');
const workflow = fs.readFileSync(workflowPath, 'utf8');
Expand Down
Loading