Skip to content
Draft
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
74 changes: 0 additions & 74 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,6 @@ on:
required: false
default: ""

# Feature toggle: Artifacts attestation for build provenance
attestation:
description: Create a verifiable attestation for the plugin using Github OIDC.
type: boolean
required: false

# User inputs
plugin-version-suffix:
description: |
Expand Down Expand Up @@ -588,7 +582,6 @@ concurrency:
permissions:
contents: write
id-token: write
attestations: write
pull-requests: read

env:
Expand Down Expand Up @@ -849,70 +842,6 @@ jobs:
dist-artifacts-retention-days: ${{ inputs.dist-artifacts-retention-days }}
DO-NOT-USE-allow-pinned-commit-hashes: ${{ inputs.DO-NOT-USE-allow-pinned-commit-hashes }}

build-attestation:
name: Build attestation
if: ${{ inputs.attestation }}
needs:
- ci
runs-on: ubuntu-x64-small

outputs:
attestation-id: ${{ steps.attestation.outputs.attestation-id }}
attestation-url: ${{ steps.attestation.outputs.attestation-url }}
bundle-path: ${{ steps.attestation.outputs.bundle-path }}
provenance-attestation: ${{ steps.provenance-attestation.outputs.provenance-attestation }}

steps:
- name: Download GitHub artifact
id: download-dist-artifacts
continue-on-error: true
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: ${{ inputs.dist-artifacts-prefix }}dist-artifacts
path: /tmp/dist-artifacts

- name: Log unavailable dist-artifacts error
if: steps.download-dist-artifacts.outcome == 'failure'
run: |
echo "::error::The dist-artifacts artifact could not be downloaded. It may have expired (retention period: ${RETENTION_DAYS} days). Please re-run the entire workflow from the beginning to rebuild the plugin."
exit 1
shell: bash
env:
RETENTION_DAYS: ${{ inputs.dist-artifacts-retention-days }}

- name: Generate artifact attestation
if: ${{ inputs.attestation }}
id: attestation
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: /tmp/dist-artifacts/*.zip

- name: Generate provenance attestation reference
if: ${{ inputs.attestation }}
id: provenance-attestation
run: |
shopt -s nullglob

universal_zip=""
for zip_path in "${DIST_ARTIFACTS_PATH}"/*.zip; do
zip_file="$(basename "${zip_path}")"
if [[ ! "${zip_file}" =~ \.[[:alnum:]]+_[[:alnum:]]+\.zip$ ]]; then
universal_zip="${zip_path}"
break
fi
done

if [ -z "${universal_zip}" ]; then
echo "::error::Could not find a universal plugin ZIP to reference in the provenance attestation."
exit 1
fi

sha256="$(sha256sum "${universal_zip}" | cut -d ' ' -f1)"
echo "provenance-attestation=github#${GITHUB_REPOSITORY_OWNER}#sha256:${sha256}" >> "${GITHUB_OUTPUT}"
shell: bash
env:
DIST_ARTIFACTS_PATH: /tmp/dist-artifacts

publish-to-catalog:
name: Publish to catalog (${{ matrix.environment }})
if: >-
Expand All @@ -923,14 +852,12 @@ jobs:
&& needs.setup.result == 'success'
&& needs.upload-to-gcs-release.result == 'success'
&& needs.ci.result == 'success'
&& (!inputs.attestation || needs.build-attestation.result == 'success')
&& needs.setup.outputs.environments != '[]'
}}
needs:
- setup
- upload-to-gcs-release
- ci
- build-attestation
strategy:
# Allow each stage to be deployed independently, even if others fails
fail-fast: false
Expand Down Expand Up @@ -1036,7 +963,6 @@ jobs:
gcloud-auth-token: ${{ steps.gcloud.outputs.id_token }}
ignore-conflicts: ${{ steps.determine-continue.outputs.ignore_conflicts }}
publish-as-pending: ${{ matrix.environment == 'prod-canary' || inputs.publish-to-catalog-as-pending }}
provenance-attestation: ${{ inputs.attestation && needs.build-attestation.outputs.provenance-attestation || '' }}

- name: Print publish summary
run: |
Expand Down
11 changes: 0 additions & 11 deletions actions/plugins/publish/publish/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ inputs:
Default is "false".
required: false
default: "false"
provenance-attestation:
description: |
Optional provenance attestation reference to include when publishing the plugin.
Expected format: github#<owner>#sha256:<zip-sha256>.
required: false
default: ""
gcom-api-url:
description: |
FOR INTERNAL TESTING ONLY.
Expand Down Expand Up @@ -81,10 +75,6 @@ runs:
publish_args+=(--publish-as-pending)
fi

if [ -n "${PROVENANCE_ATTESTATION}" ]; then
publish_args+=(--provenance-attestation "${PROVENANCE_ATTESTATION}")
fi

${{ github.action_path }}/publish.sh \
"${publish_args[@]}" \
"${args[@]}"
Expand All @@ -100,5 +90,4 @@ runs:

IGNORE_CONFLICTS: ${{ inputs.ignore-conflicts }}
PUBLISH_AS_PENDING: ${{ inputs.publish-as-pending }}
PROVENANCE_ATTESTATION: ${{ inputs.provenance-attestation }}
shell: bash
7 changes: 2 additions & 5 deletions actions/plugins/publish/publish/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ if [[ "$RUNNER_DEBUG" == "1" ]]; then
fi

usage() {
echo "Usage: $0 --environment <dev|ops|staging|prod> [--scopes <comma_separated_scopes>] [--publish-as-pending] [--provenance-attestation <attestation_reference>] [--dry-run] <plugin_zip_urls...>"
echo "Usage: $0 --environment <dev|ops|staging|prod> [--scopes <comma_separated_scopes>] [--publish-as-pending] [--dry-run] <plugin_zip_urls...>"
}

json_obj() {
Expand All @@ -16,14 +16,12 @@ gcs_zip_urls=()
scopes=''
dry_run=false
publish_as_pending=false
provenance_attestation=''
while [[ "$#" -gt 0 ]]; do
case $1 in
--environment) gcom_env=$2; shift 2;;
--scopes) scopes=$(echo $2 | jq -Rc 'split(",")'); shift 2;;
--dry-run) dry_run=true; shift;;
--publish-as-pending) publish_as_pending=true; shift;;
--provenance-attestation) provenance_attestation=$2; shift 2;;
--help)
usage
exit 0
Expand Down Expand Up @@ -139,8 +137,7 @@ json_payload=$(jq -c -n \
--arg commit "$sha" \
--argjson scopes "$scopes" \
--argjson pending "$pending_param" \
--arg provenanceAttestation "$provenance_attestation" \
'$ARGS.named | if .provenanceAttestation == "" then del(.provenanceAttestation) else . end'
'$ARGS.named'
)
echo $json_payload | jq
if [ "$dry_run" = true ]; then
Expand Down
1 change: 0 additions & 1 deletion examples/base/provisioned-plugin-auto-cd/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ jobs:
contents: write
pull-requests: read
id-token: write
attestations: write
with:
branch: ${{ github.event.inputs.branch }}
environment: ${{ github.event.inputs.environment }}
Expand Down
1 change: 0 additions & 1 deletion examples/base/provisioned-plugin-auto-cd/push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
contents: write
pull-requests: read
id-token: write
attestations: write
with:
# Checkout/build PR or main branch, depending on event
branch: ${{ github.event_name == 'push' && github.ref_name || github.ref }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ jobs:
contents: write
pull-requests: read
id-token: write
attestations: write
with:
branch: ${{ github.event.inputs.branch }}
environment: ${{ github.event.inputs.environment }}
Expand Down
1 change: 0 additions & 1 deletion examples/base/simple/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ jobs:
contents: write
pull-requests: read
id-token: write
attestations: write
with:
branch: ${{ github.event.inputs.branch }}
environment: ${{ github.event.inputs.environment }}
Expand Down
3 changes: 0 additions & 3 deletions tests/act/internal/workflow/cd/cd.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func NewWorkflow(opts ...WorkflowOption) (Workflow, error) {
Permissions: workflow.Permissions{
"contents": "write",
"id-token": "write",
"attestations": "write",
"pull-requests": "read",
},
With: map[string]any{
Expand Down Expand Up @@ -134,7 +133,6 @@ type WorkflowInputs struct {
DocsOnly *bool
AllowPublishingPRsToProd *bool
UploadGCSLatest *bool
Attestation *bool
}

// WithWorkflowInputs sets the inputs for the CD workflow.
Expand All @@ -160,7 +158,6 @@ func WithWorkflowInputs(inputs WorkflowInputs) WorkflowOption {
workflow.SetJobInput(job, "docs-only", inputs.DocsOnly)
workflow.SetJobInput(job, "allow-publishing-prs-to-prod", inputs.AllowPublishingPRsToProd)
workflow.SetJobInput(job, "upload-gcs-latest", inputs.UploadGCSLatest)
workflow.SetJobInput(job, "attestation", inputs.Attestation)
}
}

Expand Down
139 changes: 0 additions & 139 deletions tests/act/main_cd_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package main

import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"

Expand Down Expand Up @@ -262,142 +259,6 @@ func TestCD(t *testing.T) {
}
}

func TestCD_PublishIncludesProvenanceAttestation(t *testing.T) {
t.Parallel()

gitSha, err := getGitCommitSHA()
require.NoError(t, err)

const (
pluginVersion = "1.0.0"
pluginFolder = "simple-frontend"
pluginSlug = "grafana-simplefrontend-panel"
fakeGcomTokenDev = "dummy-gcom-api-key-dev"
fakeIapToken = "dummy-iap-token"
)

zipBytes, err := os.ReadFile(workflow.LocalMockdataPath(
"dist-artifacts-unsigned",
pluginFolder,
anyZipFileName(pluginSlug, pluginVersion),
))
require.NoError(t, err)
zipSHA256 := sha256.Sum256(zipBytes)
expProvenanceAttestation := fmt.Sprintf("github#grafana#sha256:%x", zipSHA256)

mockVault := workflow.VaultSecrets{
DefaultValue: newPointer(""),
CommonSecrets: map[string]string{
"plugins/gcom-publish-token:dev": fakeGcomTokenDev,
},
}

var publishCalls int
runner, err := act.NewRunner(t)
require.NoError(t, err)

runner.GCOM.HandleFunc("POST /api/plugins", func(w http.ResponseWriter, r *http.Request) {
publishCalls++

require.Subset(t, r.Header, http.Header{
"Accept": []string{"application/json"},
"User-Agent": []string{"github-actions-shared-workflows:/plugins/publish"},
"Authorization": []string{"Bearer " + fakeGcomTokenDev},
})

var body map[string]any
require.NoError(t, json.NewDecoder(r.Body).Decode(&body), "should be able to decode json body")
require.Equal(t, expProvenanceAttestation, body["provenanceAttestation"])

w.WriteHeader(http.StatusOK)
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
"plugin": map[string]any{"id": 1337},
}))
})

gcsZipURLs, err := json.Marshal([]string{gcsPublishURL(pluginSlug, pluginVersion, "any")})
require.NoError(t, err)
pluginOutput, err := json.Marshal(map[string]string{
"id": pluginSlug,
"version": pluginVersion,
})
require.NoError(t, err)

wf, err := cd.NewWorkflow(
cd.WithWorkflowInputs(cd.WorkflowInputs{
CI: ci.WorkflowInputs{
PluginDirectory: workflow.Input(filepath.Join("tests", pluginFolder)),
DistArtifactsPrefix: workflow.Input(pluginFolder + "-"),
Testing: workflow.Input(false),
},
Attestation: workflow.Input(true),
DisableDocsPublishing: workflow.Input(true),
DisableGitHubRelease: workflow.Input(true),
Environment: workflow.Input("dev"),
Scopes: workflow.Input("universal"),
}),
cd.WithMockedGCOM(t, runner.GCOM),
cd.MutateAllWorkflows().With(
workflow.WithMockedVault(t, mockVault),
),
cd.MutateCDWorkflow().With(
workflow.WithOnlyOneJob(t, "publish-to-catalog", false),
workflow.WithNoOpJobWithOutputs(t, "setup", map[string]string{
"commit-sha": gitSha,
"environments": `["dev"]`,
"publish-docs": "false",
"plugin-version-suffix": "",
"platforms": `["any"]`,
"is-release-reference": "true",
}),
workflow.WithNoOpJobWithOutputs(t, "ci", map[string]string{
"plugin": string(pluginOutput),
}),
workflow.WithNoOpJobWithOutputs(t, "upload-to-gcs-release", map[string]string{
"gcs-zip-urls": string(gcsZipURLs),
}),
workflow.WithReplacedStep(
t,
"build-attestation",
"download-dist-artifacts",
workflow.CopyMockFilesStep("dist-artifacts-unsigned/"+pluginFolder, "/tmp/dist-artifacts"),
),
workflow.WithReplacedStep(
t,
"build-attestation",
"attestation",
workflow.MockOutputsStep(map[string]string{
"attestation-id": "attestation-id",
"attestation-url": "https://github.com/grafana/plugin-ci-workflows/attestations/attestation-id",
"bundle-path": "/tmp/attestation.jsonl",
}),
),
workflow.WithEnvironment(t, "build-attestation", "provenance-attestation", map[string]string{
"GITHUB_REPOSITORY_OWNER": "grafana",
}),
workflow.WithNoOpStep(t, "publish-to-catalog", "check-and-create-stub"),
workflow.WithNoOpStep(t, "publish-to-catalog", "check-artifact-zips"),
workflow.WithReplacedStep(
t,
"publish-to-catalog",
"gcloud",
workflow.MockOutputsStep(map[string]string{
"id_token": fakeIapToken,
}),
),
workflow.WithMatrix("publish-to-catalog", map[string][]string{
"environment": {"dev"},
}),
),
)
require.NoError(t, err)

r, err := runner.Run(wf, act.NewPushEventPayload("main"))
require.NoError(t, err)
require.True(t, r.Success, "workflow should succeed")
require.Equal(t, 1, publishCalls, "GCOM API POST /plugins should be called exactly once")
}

func TestCD_GCSLatest(t *testing.T) {
// Tests that "latest" GCS release artifacts are only uploaded for release references,
// and can be forced via the upload-gcs-latest input.
Expand Down
Loading