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
47 changes: 23 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,47 +41,46 @@ jobs:

- name: Sign dist/index.js
env:
AUTHS_PASSPHRASE: ${{ secrets.AUTHS_CI_PASSPHRASE }}
AUTHS_CI_KEYCHAIN_B64: ${{ secrets.AUTHS_CI_KEYCHAIN }}
AUTHS_CI_IDENTITY_BUNDLE_B64: ${{ secrets.AUTHS_CI_IDENTITY_BUNDLE }}
AUTHS_CI_TOKEN: ${{ secrets.AUTHS_CI_TOKEN }}
AUTHS_KEYCHAIN_BACKEND: file
AUTHS_KEYCHAIN_FILE: /tmp/auths-ci-keychain
run: |
if [ -z "$AUTHS_PASSPHRASE" ] || [ -z "$AUTHS_CI_KEYCHAIN_B64" ] || [ -z "$AUTHS_CI_IDENTITY_BUNDLE_B64" ]; then
echo "::warning::Skipping artifact signing: AUTHS_CI_PASSPHRASE, AUTHS_CI_KEYCHAIN, and AUTHS_CI_IDENTITY_BUNDLE must all be set"
if [ -z "$AUTHS_CI_TOKEN" ]; then
echo "::warning::Skipping artifact signing: AUTHS_CI_TOKEN not set (run 'auths ci setup' to configure)"
exit 0
fi

printf '%s' "$AUTHS_CI_KEYCHAIN_B64" | tr -d '[:space:]' | base64 -d > /tmp/auths-ci-keychain
mkdir -p /tmp/auths-identity
printf '%s' "$AUTHS_CI_IDENTITY_BUNDLE_B64" | tr -d '[:space:]' | base64 -d | tar -xz -C /tmp/auths-identity
# Extract fields from the single CI token
AUTHS_PASSPHRASE=$(echo "$AUTHS_CI_TOKEN" | jq -r '.passphrase')
echo "::add-mask::$AUTHS_PASSPHRASE"
export AUTHS_PASSPHRASE

# Find the actual .auths dir (may be nested as .auths/ inside the tarball)
if [ -d /tmp/auths-identity/.auths ]; then
AUTHS_REPO=/tmp/auths-identity/.auths
else
AUTHS_REPO=/tmp/auths-identity
fi
echo "$AUTHS_CI_TOKEN" | jq -r '.keychain' | base64 -d > /tmp/auths-ci-keychain
mkdir -p /tmp/auths-identity
echo "$AUTHS_CI_TOKEN" | jq -r '.identity_repo' | base64 -d | tar -xz -C /tmp/auths-identity

if ! git -C "$AUTHS_REPO" rev-parse --git-dir > /dev/null 2>&1; then
echo "::warning::Skipping artifact signing: AUTHS_CI_IDENTITY_BUNDLE does not contain a valid git repository"
if ! git -C /tmp/auths-identity rev-parse --git-dir > /dev/null 2>&1; then
echo "::warning::Skipping artifact signing: identity repo in AUTHS_CI_TOKEN is not a valid git repository"
exit 0
fi

auths artifact sign dist/index.js \
--device-key ci-release-device \
--note "GitHub Actions release — ${GITHUB_REF_NAME}" \
--repo "$AUTHS_REPO"
--repo /tmp/auths-identity

echo "Signed dist/index.js → dist/index.js.auths.json"

# Write verify bundle for next step
echo "$AUTHS_CI_TOKEN" | jq -r '.verify_bundle' > /tmp/auths-verify-bundle.json

# --- Verify the artifact we just signed (dogfood) ---
- name: Verify dist/index.js attestation
if: hashFiles('dist/index.js.auths.json') != ''
uses: ./
with:
identity: ${{ secrets.AUTHS_CI_IDENTITY_BUNDLE_JSON }}
artifact-paths: 'dist/index.js'
token: /tmp/auths-verify-bundle.json
files: 'dist/index.js'
fail-on-unattested: true
fail-on-unsigned: false

Expand Down Expand Up @@ -110,17 +109,17 @@ jobs:
### Usage

```yaml
- uses: auths-dev/verify@${{ github.ref_name }}
- uses: auths-dev/verify@v1
with:
identity: '.auths/allowed_signers'
token: '.auths/allowed_signers'
```

**New: Artifact verification**
```yaml
- uses: auths-dev/verify@${{ github.ref_name }}
- uses: auths-dev/verify@v1
with:
identity: ${{ secrets.AUTHS_IDENTITY_BUNDLE }}
artifact-paths: 'dist/*.tar.gz'
token: $\{{ secrets.AUTHS_CI_TOKEN }}
files: 'dist/*.tar.gz'
```

See the [README](https://github.com/auths-dev/verify#readme) for full configuration options.
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Auths Verify Action

Verify commit signatures using [Auths](https://github.com/auths-dev/auths) identity keys. Ensures every commit in a PR or push is cryptographically signed by an authorized developer.
Verify commit signatures using [Auths](https://github.com/auths-dev/auths) token keys. Ensures every commit in a PR or push is cryptographically signed by an authorized developer.

## Quickstart

Expand All @@ -11,7 +11,7 @@ Verify commit signatures using [Auths](https://github.com/auths-dev/auths) ident
- uses: auths-dev/verify@v1
```

That's it. The action auto-detects the commit range from the GitHub event (PR or push), downloads the `auths` CLI, and verifies each commit. Identity is auto-detected from the `identity` input (defaults to `.auths/allowed_signers`).
That's it. The action auto-detects the commit range from the GitHub event (PR or push), downloads the `auths` CLI, and verifies each commit. Identity is auto-detected from the `token` input (defaults to `.auths/allowed_signers`).

## Features

Expand All @@ -29,18 +29,18 @@ That's it. The action auto-detects the commit range from the GitHub event (PR or

| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `identity` | Identity for verification. Accepts: CI token JSON, identity bundle JSON, file path to bundle, or path to allowed_signers file | No | `.auths/allowed_signers` (auto) |
| `commit-range` | Git commit range to verify (e.g. `HEAD~5..HEAD`) | No | Auto-detected from event |
| `token` | Identity for verification. Accepts: CI token JSON, identity bundle JSON, file path to bundle, or path to allowed_signers file | No | `.auths/allowed_signers` (auto) |
| `commits` | Git commit range to verify (e.g. `HEAD~5..HEAD`) | No | Auto-detected from event |
| `auths-version` | Auths CLI version to use (e.g. `0.5.0`) | No | `''` (latest) |
| `fail-on-unsigned` | Whether to fail the action if unsigned commits are found | No | `true` |
| `skip-merge-commits` | Whether to skip merge commits during verification | No | `true` |
| `post-pr-comment` | Post a PR comment with results and fix instructions (requires `pull-requests: write`) | No | `false` |
| `github-token` | GitHub token for posting the PR comment (required when `post-pr-comment: true`) | No | `''` |
| `artifact-paths` | Glob patterns for artifact files to verify, one per line | No | `''` |
| `files` | Glob patterns for artifact files to verify, one per line | No | `''` |
| `artifact-attestation-dir` | Directory containing `.auths.json` attestation files | No | `''` |
| `fail-on-unattested` | Fail the action if any artifact lacks a valid attestation | No | `true` |

The `identity` input auto-detects the format. When empty, it defaults to the `.auths/allowed_signers` file. When only `artifact-paths` is set with an identity bundle, commit verification is skipped automatically.
The `token` input auto-detects the format. When empty, it defaults to the `.auths/allowed_signers` file. When only `files` is set with an identity bundle, commit verification is skipped automatically.

## Outputs

Expand All @@ -54,11 +54,11 @@ The `identity` input auto-detects the format. When empty, it defaults to the `.a

## Verification Modes

The `identity` input auto-detects the format:
The `token` input auto-detects the format:

### Allowed Signers File (default)

Commit the team's public keys to your repo. When `identity` is empty, the action looks for `.auths/allowed_signers`:
Commit the team's public keys to your repo. When `token` is empty, the action looks for `.auths/allowed_signers`:

```
# .auths/allowed_signers
Expand All @@ -75,7 +75,7 @@ Or pass a custom path:
```yaml
- uses: auths-dev/verify@v1
with:
identity: 'path/to/allowed_signers'
token: 'path/to/allowed_signers'
```

### Identity Bundle (stateless CI)
Expand All @@ -92,15 +92,15 @@ Then pass the secret directly — the action detects the JSON format automatical
```yaml
- uses: auths-dev/verify@v1
with:
identity: ${{ secrets.AUTHS_IDENTITY_BUNDLE }}
token: ${{ secrets.AUTHS_IDENTITY_BUNDLE }}
```

Or commit the bundle (it contains only public data) and reference the file:

```yaml
- uses: auths-dev/verify@v1
with:
identity: '.auths/identity-bundle.json'
token: '.auths/token-bundle.json'
```

## Example Workflows
Expand Down Expand Up @@ -141,7 +141,7 @@ jobs:

- uses: auths-dev/verify@v1
with:
identity: ${{ secrets.AUTHS_IDENTITY_BUNDLE }}
token: ${{ secrets.AUTHS_IDENTITY_BUNDLE }}
```

### Non-blocking (Warn Only)
Expand Down Expand Up @@ -215,7 +215,7 @@ jobs:

- uses: auths-dev/verify@v1
with:
identity: ${{ secrets.AUTHS_IDENTITY_BUNDLE }}
token: ${{ secrets.AUTHS_IDENTITY_BUNDLE }}
fail-on-unsigned: ${{ inputs.mode == 'enforce' && 'true' || 'false' }}
```

Expand Down Expand Up @@ -254,6 +254,6 @@ Apache-2.0. See [LICENSE](LICENSE).

## Links

- [Auths](https://github.com/auths-dev/auths) - Decentralized identity for developers
- [Auths](https://github.com/auths-dev/auths) - Decentralized token for developers
- [Auths CLI](https://github.com/auths-dev/auths/tree/main/crates/auths-cli) - Command-line tool
- [Signing commits with Auths](https://github.com/auths-dev/auths#readme) - Setup guide
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ description: 'Verify commit signatures and artifact attestations using Auths ide
author: 'auths'

inputs:
identity:
token:
description: 'Identity for verification. Accepts: AUTHS_CI_TOKEN JSON, identity bundle JSON, file path to bundle, or path to allowed_signers file. Default: .auths/allowed_signers'
required: false
default: ''
commit-range:
commits:
description: 'Git commit range to verify (defaults to PR commits or push commits)'
required: false
default: ''
Expand All @@ -31,7 +31,7 @@ inputs:
description: 'GitHub token used to post the PR comment (required when post-pr-comment is true)'
required: false
default: ''
artifact-paths:
files:
description: 'Glob patterns for artifact files to verify, one per line (e.g., "dist/*.tar.gz")'
required: false
default: ''
Expand Down
8 changes: 4 additions & 4 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71413,11 +71413,11 @@ async function run() {
// Run pre-flight checks (shallow clone, ssh-keygen)
await (0, verifier_1.runPreflightChecks)();
// Get inputs
const identityInput = core.getInput('identity');
let commitRange = core.getInput('commit-range');
const identityInput = core.getInput('token');
let commitRange = core.getInput('commits');
const failOnUnsigned = core.getInput('fail-on-unsigned') === 'true';
const skipMergeCommits = core.getInput('skip-merge-commits') !== 'false';
const artifactPathPatterns = core.getMultilineInput('artifact-paths');
const artifactPathPatterns = core.getMultilineInput('files');
const artifactAttestationDir = core.getInput('artifact-attestation-dir');
const failOnUnattested = core.getInput('fail-on-unattested') !== 'false';
// Resolve identity (auto-detects format)
Expand Down Expand Up @@ -71561,7 +71561,7 @@ async function run() {
// Deduplicate
files = [...new Set(files)];
if (files.length === 0) {
core.warning('artifact-paths provided but no files matched');
core.warning('files provided but no artifacts matched');
}
for (const file of files) {
core.info(`Verifying artifact: ${path.basename(file)}`);
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

52 changes: 26 additions & 26 deletions src/__tests__/artifact-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,19 @@ jest.mock('../verifier', () => {

function resetMockState() {
mockInputs = {
'identity': '',
'commit-range': 'HEAD^..HEAD',
'token': '',
'commits': 'HEAD^..HEAD',
'fail-on-unsigned': 'true',
'skip-merge-commits': 'true',
'auths-version': '',
'post-pr-comment': 'false',
'github-token': '',
'artifact-paths': '',
'files': '',
'artifact-attestation-dir': '',
'fail-on-unattested': 'true',
};
mockMultilineInputs = {
'artifact-paths': [],
'files': [],
};
mockOutputs = {};
mockFailed = [];
Expand Down Expand Up @@ -154,8 +154,8 @@ describe('Artifact verification integration', () => {
jest.clearAllMocks();
});

it('does no artifact work when artifact-paths is empty', async () => {
mockMultilineInputs['artifact-paths'] = [];
it('does no artifact work when files is empty', async () => {
mockMultilineInputs['files'] = [];

await runMain();

Expand All @@ -164,9 +164,9 @@ describe('Artifact verification integration', () => {
expect(mockOutputs['artifacts-verified']).toBe('');
});

it('verifies artifacts when artifact-paths provided', async () => {
mockMultilineInputs['artifact-paths'] = ['dist/*.tar.gz'];
mockInputs['identity'] = '/tmp/bundle.json';
it('verifies artifacts when files provided', async () => {
mockMultilineInputs['files'] = ['dist/*.tar.gz'];
mockInputs['token'] = '/tmp/bundle.json';
mockGlobFiles = ['/workspace/dist/app.tar.gz'];

mockVerifyArtifact.mockResolvedValue({
Expand All @@ -191,20 +191,20 @@ describe('Artifact verification integration', () => {
expect(results[0].valid).toBe(true);
});

it('emits warning when artifact-paths matches no files', async () => {
mockMultilineInputs['artifact-paths'] = ['nonexistent/*.tar.gz'];
mockInputs['identity'] = '/tmp/bundle.json';
it('emits warning when files matches no artifacts', async () => {
mockMultilineInputs['files'] = ['nonexistent/*.tar.gz'];
mockInputs['token'] = '/tmp/bundle.json';
mockGlobFiles = [];

await runMain();

expect(mockVerifyArtifact).not.toHaveBeenCalled();
expect(mockWarnings).toContain('artifact-paths provided but no files matched');
expect(mockWarnings).toContain('files provided but no artifacts matched');
});

it('fails when fail-on-unattested is true and artifact fails', async () => {
mockMultilineInputs['artifact-paths'] = ['dist/*.tar.gz'];
mockInputs['identity'] = '/tmp/bundle.json';
mockMultilineInputs['files'] = ['dist/*.tar.gz'];
mockInputs['token'] = '/tmp/bundle.json';
mockGlobFiles = ['/workspace/dist/bad.tar.gz'];

mockVerifyArtifact.mockResolvedValue({
Expand All @@ -224,8 +224,8 @@ describe('Artifact verification integration', () => {
});

it('does not fail when fail-on-unattested is false', async () => {
mockMultilineInputs['artifact-paths'] = ['dist/*.tar.gz'];
mockInputs['identity'] = '/tmp/bundle.json';
mockMultilineInputs['files'] = ['dist/*.tar.gz'];
mockInputs['token'] = '/tmp/bundle.json';
mockInputs['fail-on-unattested'] = 'false';
mockGlobFiles = ['/workspace/dist/bad.tar.gz'];

Expand All @@ -247,10 +247,10 @@ describe('Artifact verification integration', () => {
expect(artifactFailures).toHaveLength(0);
});

it('fails when no identity bundle provided for artifact verification', async () => {
mockMultilineInputs['artifact-paths'] = ['dist/*.tar.gz'];
it('fails when no token provided for artifact verification', async () => {
mockMultilineInputs['files'] = ['dist/*.tar.gz'];
// No identity bundle set — defaults to allowed-signers
mockInputs['identity'] = '';
mockInputs['token'] = '';
mockGlobFiles = ['/workspace/dist/app.tar.gz'];

await runMain();
Expand All @@ -262,8 +262,8 @@ describe('Artifact verification integration', () => {
});

it('handles partial success correctly', async () => {
mockMultilineInputs['artifact-paths'] = ['dist/*'];
mockInputs['identity'] = '/tmp/bundle.json';
mockMultilineInputs['files'] = ['dist/*'];
mockInputs['token'] = '/tmp/bundle.json';
mockGlobFiles = ['/workspace/dist/good.tar.gz', '/workspace/dist/bad.tar.gz'];

mockVerifyArtifact
Expand Down Expand Up @@ -293,8 +293,8 @@ describe('Artifact verification integration', () => {
});

it('filters paths outside workspace', async () => {
mockMultilineInputs['artifact-paths'] = ['**/*.tar.gz'];
mockInputs['identity'] = '/tmp/bundle.json';
mockMultilineInputs['files'] = ['**/*.tar.gz'];
mockInputs['token'] = '/tmp/bundle.json';
mockGlobFiles = ['/workspace/dist/good.tar.gz', '/etc/passwd.tar.gz'];

mockVerifyArtifact.mockResolvedValue({
Expand All @@ -317,8 +317,8 @@ describe('Artifact verification integration', () => {
});

it('deduplicates glob results', async () => {
mockMultilineInputs['artifact-paths'] = ['dist/*.tar.gz', 'dist/app.tar.gz'];
mockInputs['identity'] = '/tmp/bundle.json';
mockMultilineInputs['files'] = ['dist/*.tar.gz', 'dist/app.tar.gz'];
mockInputs['token'] = '/tmp/bundle.json';
// Glob returns the same file twice from two patterns
mockGlobFiles = ['/workspace/dist/app.tar.gz', '/workspace/dist/app.tar.gz'];

Expand Down
8 changes: 4 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ async function run(): Promise<void> {
await runPreflightChecks();

// Get inputs
const identityInput = core.getInput('identity');
let commitRange = core.getInput('commit-range');
const identityInput = core.getInput('token');
let commitRange = core.getInput('commits');
const failOnUnsigned = core.getInput('fail-on-unsigned') === 'true';
const skipMergeCommits = core.getInput('skip-merge-commits') !== 'false';
const artifactPathPatterns = core.getMultilineInput('artifact-paths');
const artifactPathPatterns = core.getMultilineInput('files');
const artifactAttestationDir = core.getInput('artifact-attestation-dir');
const failOnUnattested = core.getInput('fail-on-unattested') !== 'false';

Expand Down Expand Up @@ -247,7 +247,7 @@ async function run(): Promise<void> {
files = [...new Set(files)];

if (files.length === 0) {
core.warning('artifact-paths provided but no files matched');
core.warning('files provided but no artifacts matched');
}

for (const file of files) {
Expand Down
Loading