Create Artifact Metadata Storage Record on registry push (#313)
* first pass at creating storage record Signed-off-by: Meredith Lancaster <malancas@github.com> * include storage record param in action config Signed-off-by: Meredith Lancaster <malancas@github.com> * use latest actions/attest version Signed-off-by: Meredith Lancaster <malancas@github.com> * update storage record params Signed-off-by: Meredith Lancaster <malancas@github.com> * include storage record id in result Signed-off-by: Meredith Lancaster <malancas@github.com> * regenerate dist Signed-off-by: Meredith Lancaster <malancas@github.com> * add documentation on storage records Signed-off-by: Meredith Lancaster <malancas@github.com> * log storage record creation Signed-off-by: Meredith Lancaster <malancas@github.com> * add storage record output Signed-off-by: Meredith Lancaster <malancas@github.com> * add new param Signed-off-by: Meredith Lancaster <malancas@github.com> * add storage record id output Signed-off-by: Meredith Lancaster <malancas@github.com> * fix linter errors Signed-off-by: Meredith Lancaster <malancas@github.com> * return all storage record ids Signed-off-by: Meredith Lancaster <malancas@github.com> * bump minor version Signed-off-by: Meredith Lancaster <malancas@github.com> * use expect string match function Signed-off-by: Meredith Lancaster <malancas@github.com> * add try catch block for storage record creation Signed-off-by: Meredith Lancaster <malancas@github.com> * fix table column spacing Signed-off-by: Meredith Lancaster <malancas@github.com> * check for protocol Signed-off-by: Meredith Lancaster <malancas@github.com> * check for artifact url protocol Signed-off-by: Meredith Lancaster <malancas@github.com> * only fill registry_url for now Signed-off-by: Meredith Lancaster <malancas@github.com> * cleanup protocol handling Signed-off-by: Meredith Lancaster <malancas@github.com> * regenerate dist Signed-off-by: Meredith Lancaster <malancas@github.com> * handle subject name correctly Signed-off-by: Meredith Lancaster <malancas@github.com> * move test Signed-off-by: Meredith Lancaster <malancas@github.com> * add back assert statements Signed-off-by: Meredith Lancaster <malancas@github.com> * add back output assert statements Signed-off-by: Meredith Lancaster <malancas@github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * use url for subject name parsing Signed-off-by: Meredith Lancaster <malancas@github.com> * add missing test setpu Signed-off-by: Meredith Lancaster <malancas@github.com> * fix storage record fail test Signed-off-by: Meredith Lancaster <malancas@github.com> * regenerate dist Signed-off-by: Meredith Lancaster <malancas@github.com> --------- Signed-off-by: Meredith Lancaster <malancas@github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0512723b04
commit
7667f588f2
@@ -52,11 +52,13 @@ attest:
|
||||
permissions:
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
```
|
||||
|
||||
The `id-token` permission gives the action the ability to mint the OIDC token
|
||||
necessary to request a Sigstore signing certificate. The `attestations`
|
||||
permission is necessary to persist the attestation.
|
||||
permission is necessary to persist the attestation. The `artifact-metadata`
|
||||
permission is necessary to create the artifact storage record.
|
||||
|
||||
1. Add the following to your workflow after your artifact has been built:
|
||||
|
||||
@@ -118,6 +120,12 @@ See [action.yml](action.yml)
|
||||
# the "subject-digest" parameter be specified. Defaults to false.
|
||||
push-to-registry:
|
||||
|
||||
# Whether to create a storage record for the artifact.
|
||||
# Requires that push-to-registry is set to true.
|
||||
# Requires that the "subject-name" parameter specify the fully-qualified
|
||||
# image name. Defaults to true.
|
||||
create-storage-record:
|
||||
|
||||
# Whether to attach a list of generated attestations to the workflow run
|
||||
# summary page. Defaults to true.
|
||||
show-summary:
|
||||
@@ -131,11 +139,12 @@ See [action.yml](action.yml)
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| Name | Description | Example |
|
||||
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| `attestation-id` | GitHub ID for the attestation | `123456` |
|
||||
| `attestation-url` | URL for the attestation summary | `https://github.com/foo/bar/attestations/123456` |
|
||||
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.json` |
|
||||
| Name | Description | Example |
|
||||
| ------------------- | -------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| `attestation-id` | GitHub ID for the attestation | `123456` |
|
||||
| `attestation-url` | URL for the attestation summary | `https://github.com/foo/bar/attestations/123456` |
|
||||
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.json` |
|
||||
| `storage-record-ids` | GitHub IDs for the storage records | `987654` |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
@@ -269,6 +278,10 @@ fully-qualified image name (e.g. "ghcr.io/user/app" or
|
||||
"acme.azurecr.io/user/app"). Do NOT include a tag as part of the image name --
|
||||
the specific image being attested is identified by the supplied digest.
|
||||
|
||||
If the `push-to-registry` option is set to true, the Action will also
|
||||
emit an Artifact Metadata Storage Record. If you do not want to emit a
|
||||
storage record, set `create-storage-record` to `false`.
|
||||
|
||||
> **NOTE**: When pushing to Docker Hub, please use "docker.io" as the registry
|
||||
> portion of the image name.
|
||||
|
||||
@@ -287,6 +300,7 @@ jobs:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
+45
-3
@@ -9,6 +9,7 @@ import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
|
||||
import * as oci from '@sigstore/oci'
|
||||
import * as attest from '@actions/attest'
|
||||
import fs from 'fs/promises'
|
||||
import nock from 'nock'
|
||||
import os from 'os'
|
||||
@@ -19,6 +20,7 @@ import * as main from '../src/main'
|
||||
|
||||
// Mock the GitHub Actions core library
|
||||
const infoMock = jest.spyOn(core, 'info')
|
||||
const warningMock = jest.spyOn(core, 'warning')
|
||||
const startGroupMock = jest.spyOn(core, 'startGroup')
|
||||
const setOutputMock = jest.spyOn(core, 'setOutput')
|
||||
const setFailedMock = jest.spyOn(core, 'setFailed')
|
||||
@@ -45,6 +47,7 @@ const defaultInputs: main.RunInputs = {
|
||||
subjectPath: '',
|
||||
subjectChecksums: '',
|
||||
pushToRegistry: false,
|
||||
createStorageRecord: true,
|
||||
showSummary: true,
|
||||
githubToken: '',
|
||||
privateSigning: false
|
||||
@@ -66,13 +69,14 @@ describe('action', () => {
|
||||
'base64'
|
||||
)}.}`
|
||||
|
||||
const subjectName = 'registry/foo/bar'
|
||||
const subjectName = 'ghcr.io/registry/foo/bar'
|
||||
const subjectDigest =
|
||||
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
||||
const predicate = '{}'
|
||||
const predicateType = 'https://in-toto.io/attestation/release/v0.1'
|
||||
|
||||
const attestationID = '1234567890'
|
||||
const storageRecordID = 987654321
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -82,14 +86,21 @@ describe('action', () => {
|
||||
.query({ audience: 'sigstore' })
|
||||
.reply(200, { value: oidcToken })
|
||||
|
||||
mockAgent
|
||||
.get('https://api.github.com')
|
||||
const pool = mockAgent.get('https://api.github.com')
|
||||
pool
|
||||
.intercept({
|
||||
path: /^\/repos\/.*\/.*\/attestations$/,
|
||||
method: 'post'
|
||||
})
|
||||
.reply(201, { id: attestationID })
|
||||
|
||||
pool
|
||||
.intercept({
|
||||
path: /^\/orgs\/.*\/artifacts\/metadata\/storage-record$/,
|
||||
method: 'post'
|
||||
})
|
||||
.reply(200, { storage_records: [{ id: storageRecordID }] })
|
||||
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
|
||||
@@ -263,6 +274,7 @@ describe('action', () => {
|
||||
expect(setFailedMock).not.toHaveBeenCalled()
|
||||
expect(getRegCredsSpy).toHaveBeenCalledWith(subjectName)
|
||||
expect(attachArtifactSpy).toHaveBeenCalled()
|
||||
expect(warningMock).not.toHaveBeenCalled()
|
||||
expect(infoMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.stringMatching(
|
||||
@@ -293,6 +305,14 @@ describe('action', () => {
|
||||
6,
|
||||
expect.stringMatching(attestationID)
|
||||
)
|
||||
expect(infoMock).toHaveBeenNthCalledWith(
|
||||
9,
|
||||
expect.stringMatching('Storage record created')
|
||||
)
|
||||
expect(infoMock).toHaveBeenNthCalledWith(
|
||||
10,
|
||||
expect.stringMatching('Storage record IDs: 987654321')
|
||||
)
|
||||
expect(setOutputMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'bundle-path',
|
||||
@@ -308,8 +328,30 @@ describe('action', () => {
|
||||
'attestation-url',
|
||||
expect.stringContaining(`foo/bar/attestations/${attestationID}`)
|
||||
)
|
||||
expect(setOutputMock).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'storage-record-ids',
|
||||
expect.stringMatching(storageRecordID.toString())
|
||||
)
|
||||
expect(setFailedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('catches error when storage record creation fails and continues', async () => {
|
||||
// Mock the createStorageRecord function and throw an error
|
||||
const createStorageRecordSpy = jest.spyOn(attest, 'createStorageRecord')
|
||||
createStorageRecordSpy.mockRejectedValueOnce(
|
||||
new Error('Failed to persist storage record: Not Found')
|
||||
)
|
||||
|
||||
await main.run(inputs)
|
||||
|
||||
expect(runMock).toHaveReturned()
|
||||
expect(setFailedMock).not.toHaveBeenCalled()
|
||||
expect(warningMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.stringMatching('Failed to create storage record')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the subject count is greater than 1', () => {
|
||||
|
||||
@@ -53,6 +53,12 @@ inputs:
|
||||
the "subject-digest" parameter be specified. Defaults to false.
|
||||
default: false
|
||||
required: false
|
||||
create-storage-record:
|
||||
description: >
|
||||
Whether to create a storage record for the artifact.
|
||||
Requires that push-to-registry is set to true. Defaults to true.
|
||||
default: true
|
||||
required: false
|
||||
show-summary:
|
||||
description: >
|
||||
Whether to attach a list of generated attestations to the workflow run
|
||||
@@ -71,6 +77,8 @@ outputs:
|
||||
description: 'The ID of the attestation.'
|
||||
attestation-url:
|
||||
description: 'The URL for the attestation summary.'
|
||||
storage-record-ids:
|
||||
description: 'The IDs of the storage records created for the artifact.'
|
||||
|
||||
runs:
|
||||
using: node24
|
||||
|
||||
+186
-3
@@ -1,6 +1,106 @@
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ 33354:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __rest = (this && this.__rest) || function (s, e) {
|
||||
var t = {};
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||
t[p] = s[p];
|
||||
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||
t[p[i]] = s[p[i]];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.createStorageRecord = createStorageRecord;
|
||||
const github = __importStar(__nccwpck_require__(93228));
|
||||
const plugin_retry_1 = __nccwpck_require__(33450);
|
||||
const CREATE_STORAGE_RECORD_REQUEST = 'POST /orgs/{owner}/artifacts/metadata/storage-record';
|
||||
const DEFAULT_RETRY_COUNT = 5;
|
||||
/**
|
||||
* Writes a storage record on behalf of an artifact that has been attested
|
||||
* @param artifactOptions - parameters for the storage record API request.
|
||||
* @param packageRegistryOptions - parameters for the package registry API request.
|
||||
* @param token - GitHub token used to authenticate the request.
|
||||
* @param retryAttempts - The number of retries to attempt if the request fails.
|
||||
* @param headers - Additional headers to include in the request.
|
||||
*
|
||||
* @returns The ID of the storage record.
|
||||
* @throws Error if the storage record fails to persist.
|
||||
*/
|
||||
function createStorageRecord(artifactOptions, packageRegistryOptions, token, retryAttempts, headers) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const retries = retryAttempts !== null && retryAttempts !== void 0 ? retryAttempts : DEFAULT_RETRY_COUNT;
|
||||
const octokit = github.getOctokit(token, { retry: { retries } }, plugin_retry_1.retry);
|
||||
try {
|
||||
const response = yield octokit.request(CREATE_STORAGE_RECORD_REQUEST, Object.assign({ owner: github.context.repo.owner, headers }, buildRequestParams(artifactOptions, packageRegistryOptions)));
|
||||
const data = typeof response.data == 'string'
|
||||
? JSON.parse(response.data)
|
||||
: response.data;
|
||||
return data === null || data === void 0 ? void 0 : data.storage_records.map((r) => r.id);
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : err;
|
||||
throw new Error(`Failed to persist storage record: ${message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
function buildRequestParams(artifactOptions, packageRegistryOptions) {
|
||||
const { registryUrl, artifactUrl } = packageRegistryOptions, rest = __rest(packageRegistryOptions, ["registryUrl", "artifactUrl"]);
|
||||
return Object.assign(Object.assign(Object.assign({}, artifactOptions), { registry_url: registryUrl, artifact_url: artifactUrl }), rest);
|
||||
}
|
||||
//# sourceMappingURL=artifactMetadata.js.map
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 17492:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
@@ -184,7 +284,9 @@ function buildGitHubEndpoints() {
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.buildSLSAProvenancePredicate = exports.attestProvenance = exports.attest = void 0;
|
||||
exports.buildSLSAProvenancePredicate = exports.attestProvenance = exports.attest = exports.createStorageRecord = void 0;
|
||||
var artifactMetadata_1 = __nccwpck_require__(33354);
|
||||
Object.defineProperty(exports, "createStorageRecord", ({ enumerable: true, get: function () { return artifactMetadata_1.createStorageRecord; } }));
|
||||
var attest_1 = __nccwpck_require__(17492);
|
||||
Object.defineProperty(exports, "attest", ({ enumerable: true, get: function () { return attest_1.attest; } }));
|
||||
var provenance_1 = __nccwpck_require__(13042);
|
||||
@@ -79579,15 +79681,49 @@ function wrappy (fn, cb) {
|
||||
/***/ }),
|
||||
|
||||
/***/ 93738:
|
||||
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.createAttestation = void 0;
|
||||
const attest_1 = __nccwpck_require__(11485);
|
||||
const oci_1 = __nccwpck_require__(81057);
|
||||
const subject_1 = __nccwpck_require__(36303);
|
||||
const core = __importStar(__nccwpck_require__(37484));
|
||||
const OCI_TIMEOUT = 30000;
|
||||
const OCI_RETRY = 3;
|
||||
const createAttestation = async (subjects, predicate, opts) => {
|
||||
@@ -79603,10 +79739,11 @@ const createAttestation = async (subjects, predicate, opts) => {
|
||||
if (subjects.length === 1 && opts.pushToRegistry) {
|
||||
const subject = subjects[0];
|
||||
const credentials = (0, oci_1.getRegistryCredentials)(subject.name);
|
||||
const subjectDigest = (0, subject_1.formatSubjectDigest)(subject);
|
||||
const artifact = await (0, oci_1.attachArtifactToImage)({
|
||||
credentials,
|
||||
imageName: subject.name,
|
||||
imageDigest: (0, subject_1.formatSubjectDigest)(subject),
|
||||
imageDigest: subjectDigest,
|
||||
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
|
||||
mediaType: attestation.bundle.mediaType,
|
||||
annotations: {
|
||||
@@ -79617,10 +79754,47 @@ const createAttestation = async (subjects, predicate, opts) => {
|
||||
});
|
||||
// Add the attestation's digest to the result
|
||||
result.attestationDigest = artifact.digest;
|
||||
// Because creating a storage record requires the 'artifact-metadata:write'
|
||||
// permission, we wrap this in a try/catch to avoid failing the entire
|
||||
// attestation process if the token does not have the correct permissions.
|
||||
if (opts.createStorageRecord) {
|
||||
try {
|
||||
const registryUrl = getRegistryURL(subject.name);
|
||||
const artifactOpts = {
|
||||
name: subject.name,
|
||||
digest: subjectDigest
|
||||
};
|
||||
const packageRegistryOpts = {
|
||||
registryUrl
|
||||
};
|
||||
const records = await (0, attest_1.createStorageRecord)(artifactOpts, packageRegistryOpts, opts.githubToken);
|
||||
if (!records || records.length === 0) {
|
||||
core.warning('No storage records were created.');
|
||||
}
|
||||
result.storageRecordIds = records;
|
||||
}
|
||||
catch (error) {
|
||||
core.warning(`Failed to create storage record: ${error}`);
|
||||
core.warning('Please check that the "artifact-metadata:write" permission has been included');
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
exports.createAttestation = createAttestation;
|
||||
function getRegistryURL(subjectName) {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(subjectName);
|
||||
}
|
||||
catch {
|
||||
url = new URL(`https://${subjectName}`);
|
||||
}
|
||||
if (url.protocol !== 'https:') {
|
||||
throw new Error(`Unsupported protocol ${url.protocol} in subject name ${subjectName}`);
|
||||
}
|
||||
return url.origin;
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
@@ -79690,6 +79864,7 @@ const inputs = {
|
||||
predicate: core.getInput('predicate'),
|
||||
predicatePath: core.getInput('predicate-path'),
|
||||
pushToRegistry: core.getBooleanInput('push-to-registry'),
|
||||
createStorageRecord: core.getBooleanInput('create-storage-record'),
|
||||
showSummary: core.getBooleanInput('show-summary'),
|
||||
githubToken: core.getInput('github-token'),
|
||||
// undocumented -- not part of public interface
|
||||
@@ -79790,6 +79965,7 @@ async function run(inputs) {
|
||||
const att = await (0, attest_1.createAttestation)(subjects, predicate, {
|
||||
sigstoreInstance,
|
||||
pushToRegistry: inputs.pushToRegistry,
|
||||
createStorageRecord: inputs.createStorageRecord,
|
||||
githubToken: inputs.githubToken
|
||||
});
|
||||
logAttestation(subjects, att, sigstoreInstance);
|
||||
@@ -79816,6 +79992,9 @@ async function run(inputs) {
|
||||
core.setOutput('attestation-id', att.attestationID);
|
||||
core.setOutput('attestation-url', attestationURL(att.attestationID));
|
||||
}
|
||||
if (att.storageRecordIds) {
|
||||
core.setOutput('storage-record-ids', att.storageRecordIds.join(','));
|
||||
}
|
||||
/* istanbul ignore else */
|
||||
if (inputs.showSummary) {
|
||||
await logSummary(att);
|
||||
@@ -79860,6 +80039,10 @@ const logAttestation = (subjects, attestation, sigstoreInstance) => {
|
||||
core.info(style.highlight('Attestation uploaded to registry'));
|
||||
core.info(`${subjects[0].name}@${attestation.attestationDigest}`);
|
||||
}
|
||||
if (attestation.storageRecordIds && attestation.storageRecordIds.length > 0) {
|
||||
core.info(style.highlight('Storage record created'));
|
||||
core.info(`Storage record IDs: ${attestation.storageRecordIds.join(',')}`);
|
||||
}
|
||||
};
|
||||
// Attach summary information to the GitHub Actions run
|
||||
const logSummary = async (attestation) => {
|
||||
|
||||
Generated
+10
-9
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "actions/attest",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "actions/attest",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/attest": "^2.0.0",
|
||||
"@actions/attest": "^2.1.0",
|
||||
"@actions/core": "^2.0.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@actions/glob": "^0.5.0",
|
||||
@@ -49,9 +49,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/attest": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/attest/-/attest-2.0.0.tgz",
|
||||
"integrity": "sha512-9dy7ad6B98JnXPEcXlg372fWWRKPWct1d3FSzu1/h7QtrgqE9uRlgrrx9DIHuwojJ63aaDSciXkX53Tf1Swmpw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/attest/-/attest-2.1.0.tgz",
|
||||
"integrity": "sha512-MQOaqBL1uYMZXNYUKf2aBr7Exgs2OkGIhWY3BB4hz5eIvV1x/JulQ58sz4haHYjJxcvNd5/GzuCaycfiHLLL+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
@@ -9590,9 +9591,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@actions/attest": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/attest/-/attest-2.0.0.tgz",
|
||||
"integrity": "sha512-9dy7ad6B98JnXPEcXlg372fWWRKPWct1d3FSzu1/h7QtrgqE9uRlgrrx9DIHuwojJ63aaDSciXkX53Tf1Swmpw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/attest/-/attest-2.1.0.tgz",
|
||||
"integrity": "sha512-MQOaqBL1uYMZXNYUKf2aBr7Exgs2OkGIhWY3BB4hz5eIvV1x/JulQ58sz4haHYjJxcvNd5/GzuCaycfiHLLL+g==",
|
||||
"requires": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "actions/attest",
|
||||
"description": "Generate signed attestations for workflow artifacts",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/actions/attest",
|
||||
@@ -69,7 +69,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/attest": "^2.0.0",
|
||||
"@actions/attest": "^2.1.0",
|
||||
"@actions/core": "^2.0.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@actions/glob": "^0.5.0",
|
||||
|
||||
+62
-2
@@ -1,6 +1,13 @@
|
||||
import { Attestation, Predicate, Subject, attest } from '@actions/attest'
|
||||
import {
|
||||
Attestation,
|
||||
Predicate,
|
||||
Subject,
|
||||
attest,
|
||||
createStorageRecord
|
||||
} from '@actions/attest'
|
||||
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
|
||||
import { formatSubjectDigest } from './subject'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
const OCI_TIMEOUT = 30000
|
||||
const OCI_RETRY = 3
|
||||
@@ -8,6 +15,7 @@ const OCI_RETRY = 3
|
||||
export type SigstoreInstance = 'public-good' | 'github'
|
||||
export type AttestResult = Attestation & {
|
||||
attestationDigest?: string
|
||||
storageRecordIds?: number[]
|
||||
}
|
||||
|
||||
export const createAttestation = async (
|
||||
@@ -16,6 +24,7 @@ export const createAttestation = async (
|
||||
opts: {
|
||||
sigstoreInstance: SigstoreInstance
|
||||
pushToRegistry: boolean
|
||||
createStorageRecord: boolean
|
||||
githubToken: string
|
||||
}
|
||||
): Promise<AttestResult> => {
|
||||
@@ -33,10 +42,11 @@ export const createAttestation = async (
|
||||
if (subjects.length === 1 && opts.pushToRegistry) {
|
||||
const subject = subjects[0]
|
||||
const credentials = getRegistryCredentials(subject.name)
|
||||
const subjectDigest = formatSubjectDigest(subject)
|
||||
const artifact = await attachArtifactToImage({
|
||||
credentials,
|
||||
imageName: subject.name,
|
||||
imageDigest: formatSubjectDigest(subject),
|
||||
imageDigest: subjectDigest,
|
||||
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
|
||||
mediaType: attestation.bundle.mediaType,
|
||||
annotations: {
|
||||
@@ -48,7 +58,57 @@ export const createAttestation = async (
|
||||
|
||||
// Add the attestation's digest to the result
|
||||
result.attestationDigest = artifact.digest
|
||||
|
||||
// Because creating a storage record requires the 'artifact-metadata:write'
|
||||
// permission, we wrap this in a try/catch to avoid failing the entire
|
||||
// attestation process if the token does not have the correct permissions.
|
||||
if (opts.createStorageRecord) {
|
||||
try {
|
||||
const registryUrl = getRegistryURL(subject.name)
|
||||
const artifactOpts = {
|
||||
name: subject.name,
|
||||
digest: subjectDigest
|
||||
}
|
||||
const packageRegistryOpts = {
|
||||
registryUrl
|
||||
}
|
||||
const records = await createStorageRecord(
|
||||
artifactOpts,
|
||||
packageRegistryOpts,
|
||||
opts.githubToken
|
||||
)
|
||||
|
||||
if (!records || records.length === 0) {
|
||||
core.warning('No storage records were created.')
|
||||
}
|
||||
|
||||
result.storageRecordIds = records
|
||||
} catch (error) {
|
||||
core.warning(`Failed to create storage record: ${error}`)
|
||||
core.warning(
|
||||
'Please check that the "artifact-metadata:write" permission has been included'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function getRegistryURL(subjectName: string): string {
|
||||
let url: URL
|
||||
|
||||
try {
|
||||
url = new URL(subjectName)
|
||||
} catch {
|
||||
url = new URL(`https://${subjectName}`)
|
||||
}
|
||||
|
||||
if (url.protocol !== 'https:') {
|
||||
throw new Error(
|
||||
`Unsupported protocol ${url.protocol} in subject name ${subjectName}`
|
||||
)
|
||||
}
|
||||
|
||||
return url.origin
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const inputs: RunInputs = {
|
||||
predicate: core.getInput('predicate'),
|
||||
predicatePath: core.getInput('predicate-path'),
|
||||
pushToRegistry: core.getBooleanInput('push-to-registry'),
|
||||
createStorageRecord: core.getBooleanInput('create-storage-record'),
|
||||
showSummary: core.getBooleanInput('show-summary'),
|
||||
githubToken: core.getInput('github-token'),
|
||||
// undocumented -- not part of public interface
|
||||
|
||||
+10
@@ -21,6 +21,7 @@ const ATTESTATION_PATHS_FILE_NAME = 'created_attestation_paths.txt'
|
||||
export type RunInputs = SubjectInputs &
|
||||
PredicateInputs & {
|
||||
pushToRegistry: boolean
|
||||
createStorageRecord: boolean
|
||||
githubToken: string
|
||||
showSummary: boolean
|
||||
privateSigning: boolean
|
||||
@@ -69,6 +70,7 @@ export async function run(inputs: RunInputs): Promise<void> {
|
||||
const att = await createAttestation(subjects, predicate, {
|
||||
sigstoreInstance,
|
||||
pushToRegistry: inputs.pushToRegistry,
|
||||
createStorageRecord: inputs.createStorageRecord,
|
||||
githubToken: inputs.githubToken
|
||||
})
|
||||
|
||||
@@ -100,6 +102,9 @@ export async function run(inputs: RunInputs): Promise<void> {
|
||||
core.setOutput('attestation-id', att.attestationID)
|
||||
core.setOutput('attestation-url', attestationURL(att.attestationID))
|
||||
}
|
||||
if (att.storageRecordIds) {
|
||||
core.setOutput('storage-record-ids', att.storageRecordIds.join(','))
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (inputs.showSummary) {
|
||||
@@ -169,6 +174,11 @@ const logAttestation = (
|
||||
core.info(style.highlight('Attestation uploaded to registry'))
|
||||
core.info(`${subjects[0].name}@${attestation.attestationDigest}`)
|
||||
}
|
||||
|
||||
if (attestation.storageRecordIds && attestation.storageRecordIds.length > 0) {
|
||||
core.info(style.highlight('Storage record created'))
|
||||
core.info(`Storage record IDs: ${attestation.storageRecordIds.join(',')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach summary information to the GitHub Actions run
|
||||
|
||||
Reference in New Issue
Block a user