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: 3 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: codeql

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

on:
push:
branches: [main, develop]
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: dependency-review

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

on:
pull_request:

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/quality-gate.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: quality-gate

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

on:
push:
branches: [main, develop]
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: release

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

on:
push:
tags:
- 'v*'
workflow_dispatch:

permissions:
contents: write
attestations: write
id-token: write

jobs:
release:
name: release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: '3.10'

- name: Setup uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e

- name: Build artifacts
run: uv build

- name: Generate checksums and manifest
run: |
sha256sum dist/* > dist/SHA256SUMS.txt
python scripts/release/build_release_manifest.py dist dist/release-manifest.json

- name: Upload release artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: release-artifacts
path: dist/*

- name: Attest build provenance
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be
with:
subject-path: 'dist/*'

- name: Publish GitHub release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" dist/* --generate-notes
3 changes: 3 additions & 0 deletions .github/workflows/scorecards.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: scorecards

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

on:
push:
branches: [develop]
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: tests

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

on:
push:
branches: [main, develop]
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- MinerU-backed DOM parsing API for scanned Japanese newspaper PDFs
- Synthetic newspaper fixture generation and structural equivalence checks
- Protected-branch CI, security gates, release provenance workflow, and Git Flow documentation
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pytest --cov=src/newsdom_api --cov-report=term-missing --cov-fail-under=100

CI installs dependencies from `uv.lock`, and workflow actions are pinned by immutable commit SHA. Keep both policies intact when editing `.github/` automation.

Tagged releases use `.github/workflows/release.yml` to build artifacts, generate SHA256 checksums, emit a JSON manifest, and publish a GitHub Release with provenance attestation.

## Fixture policy

This project intentionally separates public test artifacts from private validation material.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ This repository ships only synthetic test fixtures and derived structural baseli

Development setup, fixture handling rules, and local-only baseline maintenance are documented in `CONTRIBUTING.md`.

Version tags trigger a GitHub-native release workflow that builds distribution artifacts, checksums, and provenance attestations.

Project history is tracked in `CHANGELOG.md`.

Repository branch workflow is documented in `docs/workflow/git-flow.md`.

## Repository layout
Expand Down
36 changes: 36 additions & 0 deletions docs/adr/0001-openssf-best-practices-badge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ADR-0001: OpenSSF Best Practices Badge Enrollment

## Status

Accepted

## Context

The repository already has branch protection, CI checks, CodeQL, OpenSSF Scorecard, Dependabot, a security policy, locked workflow dependencies, and a planned release pipeline. Scorecard still reports a best-practices gap because the OpenSSF Best Practices badge program has not been started.

The current repository also has only one organization member and one repository collaborator, so external reviewer capacity is not yet in place. The first tagged release is not available yet because the current PR stack still needs external review before it can merge into protected branches.

## Decision

We will **defer** OpenSSF Best Practices badge enrollment until after:

1. the current protected-branch PR stack is merged,
2. the first tagged release has been produced with release provenance, and
3. at least one external reviewer is available for normal protected-branch review flow.

## Consequences

### Positive

- Keeps focus on finishing concrete repository hardening already underway.
- Avoids starting a badge questionnaire before the release and review processes are stable.
- Preserves a clear, auditable decision in the repository.

### Negative

- Scorecard will continue to report the best-practices gap until enrollment is revisited.

## Follow-up

- Revisit enrollment after issue #8 and issue #10 are resolved.
- If the repository still intends to pursue the badge at that time, assign an owner and complete the OpenSSF questionnaire.
5 changes: 5 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Architecture Decision Records

| ADR | Title | Status |
| --- | ----- | ------ |
| [0001](0001-openssf-best-practices-badge.md) | OpenSSF Best Practices Badge Enrollment | Accepted |
1 change: 1 addition & 0 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Project-local automation helpers."""
1 change: 1 addition & 0 deletions scripts/release/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Release helper scripts for build provenance and manifests."""
49 changes: 49 additions & 0 deletions scripts/release/build_release_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Build a release manifest for generated distribution artifacts."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path


def _sha256(path: Path) -> str:
"""Return the SHA-256 digest for a file."""

digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(65536), b""):
digest.update(chunk)
return digest.hexdigest()


def build_manifest(dist_dir: Path) -> dict[str, object]:
"""Build manifest metadata for all regular files in a dist directory."""

artifacts = []
for path in sorted(p for p in dist_dir.iterdir() if p.is_file()):
artifacts.append(
{
"name": path.name,
"size": path.stat().st_size,
"sha256": _sha256(path),
}
)
return {"dist_dir": str(dist_dir), "artifacts": artifacts}


def main() -> None:
"""Write the release manifest JSON for a distribution directory."""

parser = argparse.ArgumentParser()
parser.add_argument("dist_dir", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()

manifest = build_manifest(args.dist_dir)
args.output.write_text(json.dumps(manifest, indent=2), encoding="utf-8")


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src"

if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
12 changes: 12 additions & 0 deletions tests/test_adr_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pathlib import Path


def test_best_practices_decision_adr_exists_and_documents_deferral():
path = Path("docs/adr/0001-openssf-best-practices-badge.md")
assert path.exists()
text = path.read_text(encoding="utf-8")
assert "Status" in text
assert "Accepted" in text
assert "defer" in text.lower()
assert "first tagged release" in text.lower()
assert "external reviewer" in text.lower()
9 changes: 9 additions & 0 deletions tests/test_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pathlib import Path


def test_changelog_exists_and_uses_keep_a_changelog_format():
text = Path("CHANGELOG.md").read_text(encoding="utf-8")
assert text.startswith("# Changelog")
assert "Keep a Changelog" in text
assert "## [Unreleased]" in text
assert "Semantic Versioning" in text
29 changes: 29 additions & 0 deletions tests/test_release_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json
from pathlib import Path


def test_release_workflow_exists():
assert Path(".github/workflows/release.yml").exists()


def test_release_workflow_mentions_attestation_and_checksums():
text = Path(".github/workflows/release.yml").read_text(encoding="utf-8")
assert "attest-build-provenance" in text
assert "sha256sum" in text


def test_release_manifest_script_exists():
assert Path("scripts/release/build_release_manifest.py").exists()


def test_release_manifest_script_outputs_json(tmp_path: Path):
from scripts.release.build_release_manifest import build_manifest

dist = tmp_path / "dist"
dist.mkdir()
artifact = dist / "demo.txt"
artifact.write_text("demo", encoding="utf-8")
manifest = build_manifest(dist)
assert manifest["artifacts"][0]["name"] == "demo.txt"
assert manifest["artifacts"][0]["sha256"]
json.loads(json.dumps(manifest))
7 changes: 7 additions & 0 deletions tests/test_workflow_runtime_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pathlib import Path


def test_javascript_actions_are_forced_to_node24():
for workflow_path in sorted(Path(".github/workflows").glob("*.yml")):
text = workflow_path.read_text(encoding="utf-8")
assert "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true" in text, workflow_path
Loading