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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
ci:
uses: runcycles/.github/.github/workflows/ci-python.yml@v1
with:
mypy-target: runcycles_ap2
26 changes: 26 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Dependabot auto-merge

on: pull_request

# Default to read-all at top level; the automerge job below escalates only the
# narrow scopes it actually needs. Per OpenSSF Scorecard's Token-Permissions
# criterion: avoid blanket write at the workflow level.
permissions: read-all

jobs:
automerge:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]'
permissions:
contents: write # required to enable auto-merge
pull-requests: write # required to mark the PR as auto-merge
steps:
- name: Fetch Dependabot metadata
id: meta
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3
- name: Enable auto-merge for patch updates
if: steps.meta.outputs.update-type == 'version-update:semver-patch'
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
151 changes: 151 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Publish Python package

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
target:
description: "Where to publish"
required: true
default: "testpypi"
type: choice
options:
- testpypi
- pypi

# Default least-privilege; publish-* jobs override with id-token: write.
permissions:
contents: read

jobs:
build:
name: Build distributions
runs-on: ubuntu-latest

steps:
- name: Check out source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"

# Fail-fast before build/publish if (a) pyproject.toml declares a pre-release
# version (e.g. 1.0.0.dev0, 1.0.0a1) when a final-version tag (vX.Y.Z) was
# pushed, or (b) the tag version doesn't match pyproject.toml. PyPI rejects
# duplicate versions server-side, but a local guard surfaces the operator
# error before the upload phase.
- name: Verify pyproject version matches tag
if: startsWith(github.ref, 'refs/tags/v')
run: |
python -c "import tomllib,sys; v=tomllib.load(open('pyproject.toml','rb'))['project']['version']; print(v)" > /tmp/pyproject_version
PYPROJECT_VERSION=$(cat /tmp/pyproject_version)
TAG_VERSION="${GITHUB_REF_NAME#v}"
echo "pyproject.toml version: ${PYPROJECT_VERSION}"
echo "Git tag version: ${TAG_VERSION}"
if [ "${PYPROJECT_VERSION}" != "${TAG_VERSION}" ]; then
echo "::error::Version mismatch: pyproject.toml has '${PYPROJECT_VERSION}' but tag is 'v${TAG_VERSION}'. Bump pyproject.toml or retag."
exit 1
fi

- name: Install build tool
run: python -m pip install --upgrade pip build

- name: Build sdist and wheel
run: python -m build

- name: Upload distribution artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-package-distributions
path: dist/

publish-to-testpypi:
name: Publish to TestPyPI
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
environment:
name: testpypi
url: https://test.pypi.org/p/runcycles-ap2
permissions:
id-token: write
contents: read

steps:
- name: Download distribution artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: python-package-distributions
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
repository-url: https://test.pypi.org/legacy/

publish-to-pypi:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
environment:
name: pypi
url: https://pypi.org/p/runcycles-ap2
permissions:
id-token: write
contents: read

steps:
- name: Download distribution artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1

create-release:
name: Create GitHub Release
needs: publish-to-pypi
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write

steps:
- name: Check out source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Extract release notes from CHANGELOG
id: notes
run: |
version="${GITHUB_REF_NAME#v}"
# Extract the section between this version's heading and the next "## [" heading.
# Uses string functions instead of regex to stay portable across awk variants.
notes=$(awk -v v="$version" '
BEGIN { start = "## [" v "]"; flag = 0 }
substr($0, 1, length(start)) == start { flag = 1; next }
substr($0, 1, 4) == "## [" { flag = 0 }
flag { print }
' CHANGELOG.md)
if [ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then
echo "::warning::No CHANGELOG entry found for v${version} — using fallback body"
notes="Release v${version}. See commit history for changes."
fi
{
echo "notes<<EOF_GH_OUTPUT"
printf '%s\n' "$notes"
echo "EOF_GH_OUTPUT"
} >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
name: ${{ github.ref_name }}
body: ${{ steps.notes.outputs.notes }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
59 changes: 59 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenSSF Scorecard — supply-chain security analysis.
#
# Runs on push to main, on branch-protection-rule changes, and weekly so the
# score stays current as the repo evolves. Results are uploaded to GitHub's
# code-scanning surface (visible in Security tab) and published to the public
# OpenSSF metric API at api.scorecard.dev so the badge auto-updates.
#
# What it scores: branch protection, signed commits, dependency review,
# pinned dependencies, token permissions, vulnerability disclosure, fuzzing,
# SAST, and ~14 other supply-chain practices. Scores 0-10.
#
# Setup notes:
# - publish_results: true requires the repo to be public (it is).
# - id-token: write is required to mint the OIDC token used for publishing.
# - Workflow MUST live on the default branch for results to publish.
name: Scorecard supply-chain security

on:
branch_protection_rule:
schedule:
- cron: '0 6 * * 1' # Monday 06:00 UTC — weekly refresh
push:
branches: [main]

permissions: read-all

jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write # Upload SARIF to code-scanning
id-token: write # Mint OIDC token for publish to api.scorecard.dev
contents: read
actions: read
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Run analysis
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true

- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
retention-days: 5

- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
sarif_file: results.sarif
Loading