Skip to content

Commit e3f1779

Browse files
authored
test(act): add gcom mock, cd.yml changes for testing (#501)
1 parent ba46bf1 commit e3f1779

8 files changed

Lines changed: 259 additions & 44 deletions

File tree

.github/workflows/cd.yml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,14 @@ on:
342342
type: boolean
343343

344344
# Options for testing the workflow itself.
345+
dist-artifacts-prefix:
346+
description: |
347+
Prefix to use when uploading the dist artifacts as GitHub artifacts.
348+
Can be used if multiple CI jobs can be run in parallel in the same repository, in order to avoid name clashes.
349+
For example: `my-plugin-`.
350+
type: string
351+
required: false
352+
default: ""
345353
DO-NOT-USE-allow-pinned-commit-hashes:
346354
description: |
347355
FOR INTERNAL TESTING ONLY, DO NOT USE.
@@ -350,6 +358,14 @@ on:
350358
type: boolean
351359
required: false
352360
default: false
361+
DO-NOT-USE-gcom-api-url:
362+
description: |
363+
FOR INTERNAL TESTING ONLY, DO NOT USE.
364+
If set, overrides the GCOM API URL for all publish operations.
365+
Used for testing with mock GCOM servers.
366+
type: string
367+
required: false
368+
default: ""
353369

354370
concurrency:
355371
group: cd-${{ github.head_ref || github.run_id }}
@@ -397,6 +413,7 @@ jobs:
397413
with:
398414
script: |
399415
const fs = require('fs');
416+
const path = require('path');
400417
401418
// Helper to get input/env values
402419
function getInput(name, fallback = '') {
@@ -411,6 +428,7 @@ jobs:
411428
const DISABLE_DOCS_PUBLISHING = getInput('DISABLE_DOCS_PUBLISHING') === 'true';
412429
const PLUGIN_VERSION_SUFFIX = getInput('PLUGIN_VERSION_SUFFIX', '');
413430
const COMMIT_SHA = getInput('COMMIT_SHA');
431+
const PLUGIN_DIRECTORY = getInput('PLUGIN_DIRECTORY');
414432
415433
// Determine if the reference is a release one (allowed to publish to prod)
416434
let isReleaseReference = false;
@@ -426,7 +444,7 @@ jobs:
426444
// has-backend is also returned by the ci job, but we do the check here again
427445
// so we don't have to depend on the ci job.
428446
// The ci workflow uses this same check to determine if the plugin has a backend.
429-
const hasBackend = fs.existsSync('Magefile.go');
447+
const hasBackend = fs.existsSync(path.join(PLUGIN_DIRECTORY, 'Magefile.go'));
430448
let platforms = ['any'];
431449
if (hasBackend) {
432450
platforms = ['linux', 'darwin', 'windows', 'any'];
@@ -540,6 +558,7 @@ jobs:
540558
DISABLE_DOCS_PUBLISHING: ${{ inputs.disable-docs-publishing }}
541559
COMMIT_SHA: ${{ steps.checkout-specified-branch.outputs.commit }}
542560
PLUGIN_VERSION_SUFFIX: ${{ inputs.plugin-version-suffix }}
561+
PLUGIN_DIRECTORY: ${{ inputs.plugin-directory }}
543562

544563
ci:
545564
name: CI
@@ -588,6 +607,7 @@ jobs:
588607
allow-unsigned: ${{ inputs.allow-unsigned }}
589608
signature-type: ${{ inputs.signature-type }}
590609

610+
dist-artifacts-prefix: ${{ inputs.dist-artifacts-prefix }}
591611
DO-NOT-USE-allow-pinned-commit-hashes: ${{ inputs.DO-NOT-USE-allow-pinned-commit-hashes }}
592612

593613
build-attestation:
@@ -606,7 +626,7 @@ jobs:
606626
- name: Download GitHub artifact
607627
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
608628
with:
609-
name: dist-artifacts
629+
name: ${{ inputs.dist-artifacts-prefix }}dist-artifacts
610630
path: /tmp/dist-artifacts
611631

612632
- name: Generate artifact attestation
@@ -698,15 +718,18 @@ jobs:
698718
ENVIRONMENT: ${{ matrix.environment }}
699719

700720
- name: Check and create stub
721+
id: check-and-create-stub
701722
uses: grafana/plugin-ci-workflows/actions/internal/plugins/publish/check-and-create-stub@main
702723
if: ${{ matrix.environment != 'prod' && matrix.environment != 'prod-canary' }}
703724
with:
704725
plugin-id: ${{ fromJSON(needs.ci.outputs.plugin).id }}
705726
environment: ${{ matrix.environment }}
706727
gcom-publish-token: ${{ env.GCOM_PUBLISH_TOKEN }}
707728
gcloud-auth-token: ${{ steps.gcloud.outputs.id_token }}
729+
gcom-api-url: ${{ inputs.DO-NOT-USE-gcom-api-url }}
708730

709731
- name: Check artifact ZIP(s)
732+
id: check-artifact-zips
710733
uses: grafana/plugin-ci-workflows/actions/internal/plugins/publish/check-artifacts@main
711734
with:
712735
zips: ${{ needs.upload-to-gcs-release.outputs.gcs-zip-urls }}
@@ -722,6 +745,7 @@ jobs:
722745
gcloud-auth-token: ${{ steps.gcloud.outputs.id_token }}
723746
ignore-conflicts: ${{ steps.determine-continue.outputs.ignore_conflicts }}
724747
publish-as-pending: ${{ matrix.environment == 'prod-canary' || inputs.publish-to-catalog-as-pending }}
748+
gcom-api-url: ${{ inputs.DO-NOT-USE-gcom-api-url }}
725749

726750
- name: Print publish summary
727751
run: |
@@ -824,7 +848,7 @@ jobs:
824848
- name: Download GitHub artifact
825849
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
826850
with:
827-
name: dist-artifacts
851+
name: ${{ inputs.dist-artifacts-prefix }}dist-artifacts
828852
path: /tmp/dist-artifacts
829853

830854
- name: Prepare "latest" artifacts
@@ -843,6 +867,7 @@ jobs:
843867
service_account: github-plugin-ci-workflows@grafanalabs-workload-identity.iam.gserviceaccount.com
844868

845869
- name: "Set up Cloud SDK"
870+
id: gcloud-sdk
846871
uses: "google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db" # v3.0.1
847872
with:
848873
version: ">= 363.0.0"
@@ -1059,7 +1084,7 @@ jobs:
10591084
- name: Download GitHub artifact
10601085
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
10611086
with:
1062-
name: dist-artifacts
1087+
name: ${{ inputs.dist-artifacts-prefix }}dist-artifacts
10631088
path: /tmp/dist-artifacts
10641089

10651090
- name: Parse changelog

actions/internal/plugins/publish/check-and-create-stub/action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ inputs:
2626
Required only for publishing to dev or ops.
2727
required: false
2828
default: ""
29+
gcom-api-url:
30+
description: |
31+
FOR INTERNAL TESTING ONLY.
32+
Override the GCOM API URL (used for testing with mock servers).
33+
required: false
34+
default: ""
2935

3036
runs:
3137
using: composite
@@ -38,6 +44,7 @@ runs:
3844
env:
3945
GCLOUD_AUTH_TOKEN: ${{ inputs.gcloud-auth-token }}
4046
GCOM_PUBLISH_TOKEN: ${{ inputs.gcom-publish-token }}
47+
GCOM_API_URL: ${{ inputs.gcom-api-url }}
4148

4249
ENVIRONMENT: ${{ inputs.environment }}
4350
PLUGIN_ID: ${{ inputs.plugin-id }}

actions/internal/plugins/publish/check-and-create-stub/check-and-create-stub.sh

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,26 @@ if [ -z $gcom_env ]; then
3333
exit 1
3434
fi
3535

36-
# Can only be 'dev' or 'ops'. The prod stub is created manually.
37-
case $gcom_env in
38-
dev)
39-
gcom_api_url=https://grafana-dev.com/api
40-
;;
41-
ops|staging)
42-
gcom_api_url=https://grafana-ops.com/api
43-
;;
44-
*)
45-
echo "Invalid environment: $gcom_env (supported values: 'dev', 'ops')"
46-
usage
47-
exit 1
48-
;;
49-
esac
36+
# Allow URL override for testing (GCOM_API_URL takes precedence)
37+
if [ -n "$GCOM_API_URL" ]; then
38+
gcom_api_url=$GCOM_API_URL
39+
echo "Using GCOM_API_URL override: $gcom_api_url"
40+
else
41+
# Can only be 'dev' or 'ops'. The prod stub is created manually.
42+
case $gcom_env in
43+
dev)
44+
gcom_api_url=https://grafana-dev.com/api
45+
;;
46+
ops|staging)
47+
gcom_api_url=https://grafana-ops.com/api
48+
;;
49+
*)
50+
echo "Invalid environment: $gcom_env (supported values: 'dev', 'ops')"
51+
usage
52+
exit 1
53+
;;
54+
esac
55+
fi
5056

5157
# Build args for curl to GCOM (auth headers)
5258
curl_args=(

actions/plugins/publish/publish/action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ inputs:
4646
Default is "false".
4747
required: false
4848
default: "false"
49+
gcom-api-url:
50+
description: |
51+
FOR INTERNAL TESTING ONLY.
52+
Override the GCOM API URL (used for testing with mock servers).
53+
required: false
54+
default: ""
4955

5056
runs:
5157
using: composite
@@ -75,6 +81,7 @@ runs:
7581
env:
7682
GCLOUD_AUTH_TOKEN: ${{ inputs.gcloud-auth-token }}
7783
GCOM_PUBLISH_TOKEN: ${{ inputs.gcom-publish-token }}
84+
GCOM_API_URL: ${{ inputs.gcom-api-url }}
7885

7986
ZIPS_FOLDER: ${{ inputs.zips-folder }}
8087
ZIPS: ${{ inputs.zips }}

actions/plugins/publish/publish/publish.sh

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,25 +54,32 @@ if [ -z $scopes ]; then
5454
scopes='["universal"]'
5555
fi
5656

57-
has_iap=false
58-
case $gcom_env in
59-
dev)
60-
gcom_api_url=https://grafana-dev.com/api
61-
has_iap=true
62-
;;
63-
ops|staging)
64-
gcom_api_url=https://grafana-ops.com/api
65-
has_iap=true
66-
;;
67-
prod)
68-
gcom_api_url=https://grafana.com/api
69-
;;
70-
*)
71-
echo "Invalid environment: $gcom_env (supported values: 'dev', 'ops', 'staging', 'prod')"
72-
usage
73-
exit 1
74-
;;
75-
esac
57+
# Allow URL override for testing (GCOM_API_URL takes precedence)
58+
if [ -n "$GCOM_API_URL" ]; then
59+
gcom_api_url=$GCOM_API_URL
60+
has_iap=false
61+
echo "Using GCOM_API_URL override: $gcom_api_url"
62+
else
63+
has_iap=false
64+
case $gcom_env in
65+
dev)
66+
gcom_api_url=https://grafana-dev.com/api
67+
has_iap=true
68+
;;
69+
ops|staging)
70+
gcom_api_url=https://grafana-ops.com/api
71+
has_iap=true
72+
;;
73+
prod)
74+
gcom_api_url=https://grafana.com/api
75+
;;
76+
*)
77+
echo "Invalid environment: $gcom_env (supported values: 'dev', 'ops', 'staging', 'prod')"
78+
usage
79+
exit 1
80+
;;
81+
esac
82+
fi
7683

7784
# Build args for curl to GCOM (auth headers)
7885
curl_args=(

tests/act/internal/act/act.go

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"os/exec"
1313
"path/filepath"
1414
"regexp"
15+
"runtime"
1516
"strings"
1617
"testing"
1718

@@ -55,6 +56,9 @@ type Runner struct {
5556
// GCS is the GCS mock storage used during the workflow run.
5657
GCS GCS
5758

59+
// GCOM is the GCOM API mock used during the workflow run.
60+
GCOM *GCOM
61+
5862
// gitHubToken is the token used to authenticate with GitHub.
5963
gitHubToken string
6064

@@ -124,6 +128,7 @@ func NewRunner(t *testing.T, opts ...RunnerOption) (*Runner, error) {
124128
t: t,
125129
uuid: uuid.New(),
126130
gitHubToken: ghToken,
131+
GCOM: newGCOM(t),
127132
}
128133
if err := r.checkExecutables(); err != nil {
129134
return nil, err
@@ -170,11 +175,8 @@ func (r *Runner) args(eventKind EventKind, actor string, workflowFile string, pa
170175
// Required for cloning private repos
171176
"--secret", "GITHUB_TOKEN=" + r.gitHubToken,
172177

173-
// Mounts:
174-
// - mockdata: for mocked testdata, dist artifacts
175-
// - GCS: for mocked GCS
176-
// - /tmp: for temporary files, so the host's /tmp is used
177-
"--container-options", `"-v $PWD/tests/act/mockdata:/mockdata -v ` + r.GCS.basePath + `:/gcs -v /tmp:/tmp"`,
178+
// Additional Docker flags
179+
"--container-options", r.containerOptions(),
178180
}
179181
if r.actionsCachePath != "" {
180182
// Create and use per-runner cache.
@@ -402,6 +404,9 @@ func (r *Runner) parseGHACommand(data logLine, runResult *RunResult) {
402404
Title: data.KvPairs["title"],
403405
Message: data.Arg,
404406
})
407+
case "summary":
408+
// Summary
409+
runResult.Summary = append(runResult.Summary, data.Content)
405410
default:
406411
// Nothing special to do
407412
if r.Verbose && data.Command != "" {
@@ -477,6 +482,9 @@ type RunResult struct {
477482

478483
// Annotations contains the GitHub Actions annotations generated during the workflow run.
479484
Annotations []Annotation
485+
486+
// Summary contains the summary of the workflow run.
487+
Summary []string
480488
}
481489

482490
// AnnotationLevel represents the level of a GitHub Actions annotation.
@@ -518,6 +526,64 @@ func (r *RunResult) GetTestingWorkflowRunID() (string, error) {
518526
return runID, nil
519527
}
520528

529+
// containerOptions returns the Docker container options for act.
530+
// On Linux, it adds --add-host to enable host.docker.internal (already works on Docker Desktop for macOS/Windows).
531+
func (r *Runner) containerOptions() string {
532+
533+
opts := []string{
534+
// mocked testdata, dist artifacts
535+
"-v $PWD/tests/act/mockdata:/mockdata",
536+
// mocked GCS
537+
"-v " + r.GCS.basePath + ":/gcs",
538+
}
539+
540+
// On Linux, add --add-host for host.docker.internal (Docker Desktop handles this automatically)
541+
// This is needed for local mock HTTP servers (e.g.: GCOM)
542+
if runtime.GOOS == "linux" {
543+
if hostIP := getDockerHostIP(); hostIP != "" {
544+
opts = append([]string{"--add-host=host.docker.internal:" + hostIP}, opts...)
545+
}
546+
}
547+
548+
return `"` + strings.Join(opts, " ") + `"`
549+
}
550+
551+
// getDockerHostIP returns the IP address that Docker containers can use to reach the host.
552+
// On Linux, this is typically the docker0 bridge IP (172.17.0.1) or the IP from the default route.
553+
// Returns empty string if detection fails (the mock servers won't work, but at least act will start).
554+
func getDockerHostIP() string {
555+
// Try to get the IP from the default route (most reliable method)
556+
cmd := exec.Command("ip", "route", "get", "1")
557+
output, err := cmd.Output()
558+
if err == nil {
559+
// Output looks like: "1.0.0.0 via 192.168.1.1 dev eth0 src 192.168.1.100 uid 1000"
560+
// We want the "src" IP
561+
fields := strings.Fields(string(output))
562+
for i, field := range fields {
563+
if field == "src" && i+1 < len(fields) {
564+
return fields[i+1]
565+
}
566+
}
567+
}
568+
569+
// Fallback: try docker0 bridge IP (common default)
570+
iface, err := net.InterfaceByName("docker0")
571+
if err == nil {
572+
addrs, err := iface.Addrs()
573+
if err == nil && len(addrs) > 0 {
574+
// Get the first IPv4 address
575+
for _, addr := range addrs {
576+
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil {
577+
return ipnet.IP.String()
578+
}
579+
}
580+
}
581+
}
582+
583+
// Last resort: use the common docker bridge IP
584+
return "172.17.0.1"
585+
}
586+
521587
// getFreePort asks the kernel for a free open port that is ready to use.
522588
func getFreePort() (port int, err error) {
523589
var a *net.TCPAddr

0 commit comments

Comments
 (0)