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
71 changes: 71 additions & 0 deletions .github/actions/install/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Install grcli
description: >-
Install the grcli binary from the public GHCR OCI artifact onto the
runner PATH. No token required (the package is public).

inputs:
version:
description: >-
Version tag to install from ghcr.io/gemaraproj/grcli, e.g. v0.1.0,
or "latest".
required: false
default: latest
verify:
description: >-
Verify the cosign signature before installing. Requires cosign on
PATH (e.g. a prior sigstore/cosign-installer step). Default false.
required: false
default: "false"

runs:
using: composite
steps:
# v2: https://github.com/oras-project/setup-oras/releases/tag/v2.0.0
- uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3
with:
version: 1.3.0

- shell: bash
env:
GRCLI_VERSION: ${{ inputs.version }}
GRCLI_VERIFY: ${{ inputs.verify }}
run: |
Comment thread
eddie-knight marked this conversation as resolved.
set -euo pipefail
image="ghcr.io/gemaraproj/grcli"

case "${RUNNER_OS}" in
Linux) os=linux ;;
macOS) os=darwin ;;
Windows) os=windows ;;
*) echo "grcli install: unsupported RUNNER_OS=${RUNNER_OS}" >&2; exit 1 ;;
esac
case "${RUNNER_ARCH}" in
X64) arch=amd64 ;;
ARM64) arch=arm64 ;;
*) echo "grcli install: unsupported RUNNER_ARCH=${RUNNER_ARCH}" >&2; exit 1 ;;
esac
bin=grcli; [ "$os" = "windows" ] && bin=grcli.exe

# Resolve the tag once and use the digest for both verify and pull, so a
# retag between the two steps cannot install a binary that was never
# verified.
ref="${image}@$(oras resolve "${image}:${GRCLI_VERSION}")"

if [ "${GRCLI_VERIFY}" = "true" ]; then
if ! command -v cosign >/dev/null 2>&1; then
echo "grcli install: verify=true but cosign not on PATH; add a sigstore/cosign-installer step first" >&2
exit 1
fi
cosign verify "$ref" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--certificate-identity-regexp "^https://github.com/gemaraproj/grcli/.github/workflows/release.yml@" \
>/dev/null
fi

dest="${RUNNER_TEMP}/grcli-bin"
mkdir -p "$dest"
oras pull "$ref" --platform "${os}/${arch}" -o "$dest"
chmod +x "${dest}/${bin}" 2>/dev/null || true

echo "$dest" >> "$GITHUB_PATH"
"${dest}/${bin}" --version
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: ci

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: go.mod
cache: true
- run: make fmtcheck
- run: make vet
- run: make tidycheck
- run: make testcov
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.12.2
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: go.mod
cache: true
- run: make build
92 changes: 92 additions & 0 deletions .github/workflows/publish-gemara.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Reusable workflow: publish Gemara artifact(s) to grc.store with grcli.
#
# The artifact TYPE (catalog, threat model, evaluation log, …) is NOT a
# parameter — grcli reads it from each file's Gemara `metadata`, so this one
# workflow publishes any Gemara type. You supply the file(s), the license, and
# optionally the hub URL.
#
# === AUTH: NO SECRET REQUIRED (trusted publishing) ===
# grcli uses the workflow's GitHub Actions OIDC token as its hub credential and
# for cosign keyless signing. `id-token: write` is the entire auth setup — do
# NOT add GRCLI_TOKEN, a PAT, or any `secrets.*` reference.
#
# Prerequisite (one-time, by a hub/org admin — NOT in the caller's repo):
# register the calling repository (owner/repo, optionally ref-pinned) as a
# Trusted CI publisher for the target namespace. Without that binding the hub
# returns 403; adding a GitHub secret will not fix it.
#
# --- Call it from a catalog repo ---
# name: Publish catalog
# on:
# push:
# branches: [main]
# paths: ['controls.yaml']
# workflow_dispatch: {}
# jobs:
# publish:
# permissions:
# contents: read
# id-token: write # caller MUST grant this — it's what auth uses
# uses: gemaraproj/grcli/.github/workflows/publish-gemara.yml@v0.1.0
# with:
# files: controls.yaml
# license: Apache-2.0

name: Publish Gemara artifact

on:
workflow_call:
inputs:
files:
description: 'Artifact file(s), whitespace/newline-separated. Each entry is published as one artifact; grcli reads its type + version from the YAML.'
required: true
type: string
license:
description: 'REQUIRED publication license as an SPDX expression (e.g. Apache-2.0, MIT OR Apache-2.0, or a LicenseRef-… token). Applied to every file.'
required: true
type: string
hub-url:
description: 'grc.store hub base URL — discovers the registry and is the sync target.'
required: false
type: string
default: https://hub.grc.store
grcli-version:
description: 'grcli release tag to install from ghcr.io/gemaraproj/grcli.'
required: false
type: string
default: v0.1.0

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # hub auth + cosign keyless signing (no secret)
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

# grcli ships as a public, signed, multi-platform OCI artifact; pulling
# needs no token. v2: https://github.com/oras-project/setup-oras/releases/tag/v2.0.0
- uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3
- name: Install grcli ${{ inputs.grcli-version }}
env:
GRCLI_VERSION: ${{ inputs.grcli-version }}
run: |
Comment thread
eddie-knight marked this conversation as resolved.
oras pull "ghcr.io/gemaraproj/grcli:$GRCLI_VERSION" --platform linux/amd64
sudo install grcli /usr/local/bin/grcli

- name: Publish
env:
FILES: ${{ inputs.files }}
LICENSE: ${{ inputs.license }}
HUB_URL: ${{ inputs.hub-url }}
run: |
set -euo pipefail
# Word-split FILES on whitespace/newlines: one grcli publish per artifact.
for f in $FILES; do
echo "::group::grcli publish $f"
grcli publish -f "$f" --license "$LICENSE" --url "$HUB_URL"
echo "::endgroup::"
done
95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: release

# Publish grcli as a public, multi-platform OCI artifact on GHCR, signed
# keyless with cosign. Triggered by pushing a semver tag (e.g. v0.1.0).
#
# The published *package* is public, so anyone can `oras pull` the binary
# with no token. See README "Install or Upgrade".
#
# Native macOS binaries ride along because these are raw OCI artifacts,
# not container images (images can't carry darwin binaries).

on:
push:
tags: ['v*']

permissions:
contents: read
packages: write # push the OCI artifact to GHCR
id-token: write # cosign keyless signing (Sigstore via GitHub OIDC)

env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/${{ github.repository }} # ghcr.io/gemaraproj/grcli

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: go.mod
cache: false # release job holds packages:write + id-token:write; no cache poisoning surface (Kusari)

# oras >= 1.3 is required for --artifact-platform and
# `oras manifest index create`.
# v2: https://github.com/oras-project/setup-oras/releases/tag/v2.0.0
- uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3
with:
version: 1.3.0

- uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2

- name: Log in to GHCR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ACTOR: ${{ github.actor }}
run: echo "$GH_TOKEN" | oras login "$REGISTRY" -u "$ACTOR" --password-stdin

- name: Build binaries, push per-platform artifacts, assemble index
id: build
env:
VERSION: ${{ github.ref_name }}
CGO_ENABLED: "0"
run: |
set -euo pipefail
ldflags="-s -w -X github.com/gemaraproj/grcli/cmd.version=${VERSION}"
artifact_type="application/vnd.gemaraproj.grcli.binary"
platforms="linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64"

children=()
for p in $platforms; do
os="${p%/*}"; arch="${p#*/}"
bin="grcli"; [ "$os" = "windows" ] && bin="grcli.exe"
echo "::group::build & push $p"
GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags "$ldflags" -o "$bin" .
tag="${VERSION}-${os}-${arch}"
# Push the raw binary as a platform-tagged OCI artifact. The file
# is stored under its own name, so `oras pull` restores `grcli`.
oras push --artifact-type "$artifact_type" \
--artifact-platform "$os/$arch" \
"${IMAGE}:${tag}" \
"${bin}:application/octet-stream"
rm -f "$bin"
children+=( "${IMAGE}:${tag}" )
echo "::endgroup::"
done

# Combine the per-platform artifacts into one multi-arch index.
# Platform metadata is carried from each child (set above), so a
# later `oras pull --platform os/arch` selects the right binary.
oras manifest index create "${IMAGE}:${VERSION}" "${children[@]}"
oras tag "${IMAGE}:${VERSION}" latest

digest="$(oras resolve "${IMAGE}:${VERSION}")"
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
echo "Published ${IMAGE}:${VERSION} (${digest})"

# Sign by digest. The signature covers the index, hence every tag
# (version and latest) that points at it.
- name: Sign the index (keyless)
run: cosign sign --yes "${IMAGE}@${{ steps.build.outputs.digest }}"
33 changes: 6 additions & 27 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
bin/
coverage.out
*.test

# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
grcli-out/
.grcli.yaml
.env
go.work
go.work.sum

# env file
.env

# Editor/IDE
# .idea/
# .vscode/
.claude/
10 changes: 10 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: "2"

# golangci-lint v2. The default linter set (standard) is kept; we only
# re-enable the "std-error-handling" exclusion preset, which v1 applied by
# default but v2 makes opt-in. It excludes errcheck on fire-and-forget
# writes to stdout/stderr (fmt.Fprint*, etc.).
linters:
exclusions:
presets:
- std-error-handling
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog

Notable changes to `grcli`. This project is pre-1.0; while on `v0.x`, a breaking
change bumps the minor version.

## [0.1.0] - Unreleased

First release of `grcli` under `github.com/gemaraproj/grcli`, published as a
signed multi-platform OCI artifact at `ghcr.io/gemaraproj/grcli`.

### Added

- `login` / `logout` — OIDC device-flow sign-in to a hub, credentials stored
at `$XDG_DATA_HOME/grcli/credentials.json`.
- `validate` — check Gemara YAML against the spec via `cue vet`.
- `publish` — pack an artifact plus SLSA-shaped provenance into an OCI bundle,
sign it, push it, and notify the hub. `--license` (an SPDX expression) is
required. Keyless signing runs in-process via `sigstore-go` using the
GitHub Actions OIDC token; `cosign` is needed only for `--cosign-key`.
- `verify` — verify a bundle's Sigstore signature in-process; with no trust
flags, against the signer identity the hub recorded at ingest.
- `unpack` — verify (fail-closed; `--no-verify` to skip) then extract a bundle
to a directory from a registry or OCI layout.
- `cat` — stream an artifact's Gemara content to stdout without writing files.
- `versions <ns>/<id>` — list published versions.
- On-disk cache for remote fetches at `$GRCLI_CACHE`; `--no-cache` per run,
`cache-enabled: false` to disable.
- Single user-global config at `$XDG_CONFIG_HOME/grcli/config.yaml`, with
`GRCLI_*` env overrides and `--config <file>` to bypass.
- Reusable GitHub Actions workflow (`.github/workflows/publish-gemara.yml`)
and install action (`.github/actions/install`).
Loading
Loading