diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 7b37d26..0000000 --- a/.drone.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: pipeline -type: docker -name: publish - -steps: - - name: github-release - image: golang:1.24-alpine3.21 - environment: - GITHUB_APP_ID: - from_secret: gh-app-id - GITHUB_APP_INSTALLATION_ID: - from_secret: gh-app-installation-id - GITHUB_APP_PRIVATE_KEY: - from_secret: gh-app-private-key - commands: - - apk add git - - - cd /tmp - - git clone https://github.com/magefile/mage - - cd mage - - go run bootstrap.go - - - cd /drone/src - - mage gitHub:release $DRONE_TAG - -trigger: - event: - include: - - tag - ---- -name: gh-app-private-key -kind: secret -get: - name: private-key - path: infra/data/ci/detect-angular-dashboards/github-app ---- -name: gh-app-installation-id -kind: secret -get: - name: app-installation-id - path: infra/data/ci/detect-angular-dashboards/github-app ---- -name: gh-app-id -kind: secret -get: - name: app-id - path: infra/data/ci/detect-angular-dashboards/github-app - ---- -kind: signature -hmac: 408f85bd86c07da20975a9d327c1304891dd02356df5a545f3bb67e6c8659701 - -... diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..bae0c1e --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,17 @@ +env: + GO_VERSION: "1.24" + MAGE_VERSION: v1.15.0 + +runs: + using: composite + steps: + - name: Setup Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Mage + uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3.1.0 + with: + version: ${{ env.MAGE_VERSION }} + install-only: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0cd33b0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + branches: + - main + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + GOLANGCI_LINT_VERSION: v2.5.0 + +jobs: + lint: + name: Lint + runs-on: ubuntu-arm64-small + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: Setup + uses: ./.github/actions/setup + - name: Lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + version: ${{ env.GOLANGCI_LINT_VERSION }} + args: | + "./..." --timeout=7m + + test: + name: Test + runs-on: ubuntu-arm64-small + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: Setup + uses: ./.github/actions/setup + - name: Test + run: mage test + shell: bash diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..f9cc7ea --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,22 @@ +name: PR checks + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + branches: + - main + +jobs: + check-pr-title: + name: Check title + permissions: + pull-requests: read + runs-on: ubuntu-arm64-small + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..980a374 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,74 @@ +on: + push: + branches: + - main + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-arm64-small + + permissions: + contents: write # Needed to create releases and tags + pull-requests: write # Needed to create release PRs + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Get secrets from Vault + id: get-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@a37de51f3d713a30a9e4b21bcdfbd38170020593 # get-vault-secrets/v1.3.0 + with: + common_secrets: | + GITHUB_APP_ID=plugins-platform-bot-app:app-id + GITHUB_APP_PRIVATE_KEY=plugins-platform-bot-app:private-key + export_env: false + + - name: Generate GitHub token + id: generate-github-token + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + with: + app-id: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_ID }} + private-key: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: Release Please + id: release + uses: googleapis/release-please-action@c2a5a2bd6a758a0937f1ddb1e8950609867ed15c # v4.3.0 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: ${{ github.ref_name }} + token: ${{ steps.generate-github-token.outputs.token }} + + # Whenever there is a new release, build detect-angular-dashboards + # with the new version in the linker fields and attach the build artifacts to the GitHub release. + + - name: Setup + if: ${{ steps.release.outputs.release_created }} + uses: ./.github/actions/setup + + - name: Build and package all + if: ${{ steps.release.outputs.release_created }} + run: | + mage package "${PACKAGE_NAME}" + env: + PACKAGE_NAME: ${{ steps.release.outputs.version }} + + - name: Attach build artifacts to release + if: ${{ steps.release.outputs.release_created }} + run: | + gh release upload "${TAG_NAME}" ./dist/artifacts/${TAG_NAME}/*.zip + shell: bash + env: + GITHUB_TOKEN: ${{ steps.generate-github-token.outputs.token }} + TAG_NAME: ${{ steps.release.outputs.tag_name }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..7d9b009 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.10.0" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c86e626..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,13 +0,0 @@ -# Releasing a new version -Releases are done through [Drone](https://drone.grafana.net/grafana/detect-angular-dashboards). - -Once the changes are into the `main` branch and you're ready to cut a new release, create and push an annotated tag for the new release: - -```bash -git tag -a v0.7.0 -m "v0.7.0" -git push origin v0.7.0 -``` - -At this point, a Drone pipeline will start and create a GitHub Release. You can follow its progress [here](https://drone.grafana.net/grafana/detect-angular-dashboards). - -Once the release appears in the GitHub "[releases](https://github.com/grafana/detect-angular-dashboards/releases)" section, you can manually edit its description to list the relevant changes. diff --git a/Magefile.go b/Magefile.go index 13470ed..8a45593 100644 --- a/Magefile.go +++ b/Magefile.go @@ -4,22 +4,17 @@ package main import ( "archive/zip" - "context" + "encoding/json" "errors" "fmt" "io" "io/fs" - "net/http" "os" "path/filepath" "runtime" - "strconv" "strings" "sync" - "time" - "github.com/bradleyfalzon/ghinstallation/v2" - "github.com/google/go-github/v53/github" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" ) @@ -29,18 +24,13 @@ type Build mg.Namespace const ( distFolder = "dist" artifactsFolder = "artifacts" - - droneServerURL = "https://drone.grafana.net" - gitHubOrg = "grafana" - gitHubRepo = "detect-angular-dashboards" - droneRepo = gitHubOrg + "/" + gitHubRepo + programName = "detect-angular-dashboards" ) // Go builds the go binary for the specified os and arch into dist/_/detect-angular-dashboards. func (Build) Go(goOs, goArch string) error { fmt.Println("building for", goOs, goArch) - const programName = "detect-angular-dashboards" args := []string{"build", "-o", filepath.Join(distFolder, goOs+"_"+goArch, programName)} ldFlags := []string{"-s", "-w"} @@ -50,9 +40,9 @@ func (Build) Go(goOs, goArch string) error { // If commit sha was determined, add it to ldflags ldFlags = append(ldFlags, fmt.Sprintf("-X %s.LinkerCommitSHA=%s", buildPkg, commitSha)) } - if droneTag := os.Getenv("DRONE_TAG"); droneTag != "" { - // Add drone tag as linker version - ldFlags = append(ldFlags, fmt.Sprintf("-X %s.LinkerVersion=%s", buildPkg, droneTag)) + if version, err := releasePleaseVersion(); err == nil && version != "" { + // Add release please version as linker version + ldFlags = append(ldFlags, fmt.Sprintf("-X %s.LinkerVersion=%s", buildPkg, version)) } // Add all ldflags to args @@ -133,7 +123,7 @@ func (b Build) zipFolder(inFolder string, outFileName string) error { // Docker builds the docker image with the specified tag. func (Build) Docker(tag string) error { - return sh.RunV("docker", "build", "-t", "detect-angular-dashboards:"+tag, ".") + return sh.RunV("docker", "build", "-t", programName+":"+tag, ".") } // Package runs build:all and creates multiple .zip files inside dist/artifacts/, one for each folder in dist/*. @@ -167,7 +157,7 @@ func Package(releaseName string) error { if err := os.MkdirAll(outFolder, os.ModePerm); err != nil { return fmt.Errorf("mkdir %q: %w", outFolder, err) } - zipFn := filepath.Join(outFolder, fmt.Sprintf("%s_%s_%s.zip", gitHubRepo, releaseName, d.Name())) + zipFn := filepath.Join(outFolder, fmt.Sprintf("%s_%s_%s.zip", programName, releaseName, d.Name())) wg.Add(1) go func() { @@ -200,7 +190,7 @@ func Clean() error { // Test runs the test suite. func Test() error { - return sh.RunV("go", "test", "./...") + return sh.RunV("go", "test", "-v", "./...") } // Lint runs golangci-lint. @@ -212,146 +202,25 @@ func Lint() error { return nil } -// Drone runs drone lint to ensure .drone.yml is valid and it signs the Drone configuration file. -// This needs to be run everytime the .drone.yml file is modified. -// See https://github.com/grafana/deployment_tools/blob/master/docs/infrastructure/drone/signing.md for more info -func Drone() error { - if err := sh.RunV("drone", "lint", "--trusted"); err != nil { - return err - } - if err := sh.RunV("drone", "--server", droneServerURL, "sign", "--save", droneRepo); err != nil { - return err - } - return nil -} - -type GitHub mg.Namespace - -// Release pushes a GitHub release -func (g GitHub) Release(releaseName string) error { - mg.Deps(mg.F(Package, releaseName)) - - // Determine files to upload - artifactsRoot := filepath.Join(distFolder, artifactsFolder, releaseName) - toUploadFileNames := map[string]struct{}{} - if err := filepath.WalkDir(artifactsRoot, func(path string, d fs.DirEntry, err error) error { - if path == artifactsRoot { - // Skip folder itself - return nil - } - if d.IsDir() { - // Do not recurse - return filepath.SkipDir - } - if filepath.Ext(d.Name()) != ".zip" { - // Skip non-zip files - return nil - } - toUploadFileNames[filepath.Join(artifactsRoot, d.Name())] = struct{}{} - return nil - }); err != nil { - return fmt.Errorf("walkdir: %w", err) - } - - // Ensure we have files to attach to the release - if len(toUploadFileNames) == 0 { - return fmt.Errorf("could not find artifacts to upload for %q", releaseName) - } - - // Check and get GitHub app env vars - var ghAppID, ghInstallationID int64 - for _, o := range []struct { - dst *int64 - envVar string - }{ - {&ghAppID, "GITHUB_APP_ID"}, - {&ghInstallationID, "GITHUB_APP_INSTALLATION_ID"}, - } { - var err error - v := os.Getenv(o.envVar) - *o.dst, err = strconv.ParseInt(v, 10, 64) - if err != nil { - return fmt.Errorf("%q (value of env var %q) is not an integer", v, o.envVar) - } - } - - // Create GitHub client - ghTransport, err := ghinstallation.New(http.DefaultTransport, ghAppID, ghInstallationID, []byte(os.Getenv("GITHUB_APP_PRIVATE_KEY"))) - if err != nil { - return fmt.Errorf("ghinstallation new: %w", err) - } - ghClient := github.NewClient(&http.Client{Transport: ghTransport}) - ctx, canc := context.WithTimeout(context.Background(), time.Minute*10) - defer canc() - - // Create release - release, _, err := ghClient.Repositories.CreateRelease(ctx, gitHubOrg, gitHubRepo, &github.RepositoryRelease{ - Name: github.String(releaseName), - TagName: github.String(releaseName), - Draft: github.Bool(false), - Prerelease: github.Bool(false), - MakeLatest: github.String("true"), - }) - if err != nil { - return fmt.Errorf("create github release: %w", err) - } - fmt.Println("created github release", releaseName) - - // Set up error handling - var finalErr error - errs := make(chan error) - go func() { - for err := range errs { - finalErr = errors.Join(finalErr, err) - } - }() - - // Upload all artifacts and attach them to the release - var wg sync.WaitGroup - wg.Add(len(toUploadFileNames)) - for fn := range toUploadFileNames { - fn := fn - go func() { - defer wg.Done() - - fmt.Println("uploading", fn, "...") - f, err := os.Open(fn) - if err != nil { - errs <- fmt.Errorf("open %q: %w", fn, err) - return - } - defer func() { - if err := f.Close(); err != nil && !errors.Is(err, os.ErrClosed) { - errs <- fmt.Errorf("close %q: %w", fn, err) - } - }() - - if _, _, err := ghClient.Repositories.UploadReleaseAsset(ctx, gitHubOrg, gitHubRepo, *release.ID, &github.UploadOptions{ - Name: filepath.Base(fn), - }, f); err != nil { - errs <- fmt.Errorf("upload release artifact %q: %w", fn, err) - return - } - fmt.Println("upload", fn, "ok!") - }() - } - - // Wait for upload goroutines to finish - wg.Wait() - close(errs) - return finalErr -} - // gitCommitSha returns the git commit sha for the current repo or "" if none. -// It tries to get it from DRONE_COMMIT_SHA env var (set from drone). -// If it's not set, it invokes `git`. +// It invokes `git` to determine the commit sha. // If it's not possible to run `git`, it returns an empty string. func gitCommitSha() string { - // Try to get git commit sha, prioritize env var from drone - if commitSha := os.Getenv("DRONE_COMMIT_SHA"); commitSha != "" { - return commitSha - } - // If not possible, try invoking `git` command hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD") return hash } + +// releasePleaseVersion reads the .release-please-manifest.json file and returns the version for the current component ("."). +func releasePleaseVersion() (string, error) { + f, err := os.Open(".release-please-manifest.json") + if err != nil { + return "", err + } + defer func() { _ = f.Close() }() + var manifest map[string]string + if err := json.NewDecoder(f).Decode(&manifest); err != nil { + return "", err + } + const releasePleaseComponent = "." + return manifest[releasePleaseComponent], nil +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..88aec32 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "changelog-sections": [ + { + "section": "🎉 Features", + "type": "feat" + }, + { + "section": "🐛 Bug Fixes", + "type": "fix" + }, + { + "section": "📝 Documentation", + "type": "docs" + }, + { + "section": "💄 Styles", + "type": "style" + }, + { + "section": "♻️ Code Refactoring", + "type": "refactor" + }, + { + "section": "⚡ Performance Improvements", + "type": "perf" + }, + { + "section": "✅ Tests", + "type": "test" + }, + { + "section": "🏗️ Builds", + "type": "build" + }, + { + "section": "🤖 Continuous Integrations", + "type": "ci" + }, + { + "section": "🔧 Chores", + "type": "chore" + }, + { + "section": "⏪ Reverts", + "type": "revert" + } + ], + "draft-pull-request": true, + "include-v-in-tag": true, + "packages": { + ".": {} + }, + "release-type": "go" +}