From 130067e5f23e0d0e974e4ebd27380b66c920517e Mon Sep 17 00:00:00 2001 From: OpenTofu Core Development Team Date: Fri, 17 Nov 2023 14:13:16 +0200 Subject: [PATCH] Apply GitHub workflow changes --- .../workflows/artifact-release-trigger.yml | 12 ++ .github/workflows/artifact-release.yml | 33 ++++ .github/workflows/fork_sync.yml | 11 ++ .github/workflows/go/sign/go.mod | 18 ++ .github/workflows/go/sign/go.sum | 46 +++++ .github/workflows/go/sign/main.go | 183 ++++++++++++++++++ .github/workflows/resign.yml | 17 ++ 7 files changed, 320 insertions(+) create mode 100644 .github/workflows/artifact-release-trigger.yml create mode 100644 .github/workflows/artifact-release.yml create mode 100644 .github/workflows/fork_sync.yml create mode 100644 .github/workflows/go/sign/go.mod create mode 100644 .github/workflows/go/sign/go.sum create mode 100644 .github/workflows/go/sign/main.go create mode 100644 .github/workflows/resign.yml diff --git a/.github/workflows/artifact-release-trigger.yml b/.github/workflows/artifact-release-trigger.yml new file mode 100644 index 0000000000..5f12560dfd --- /dev/null +++ b/.github/workflows/artifact-release-trigger.yml @@ -0,0 +1,12 @@ +name: Trigger Artifact Release + +on: + workflow_dispatch: + +permissions: + contents: read + actions: write + +jobs: + trigger: + uses: opentffoundation/scripts/.github/workflows/trigger.yml@main diff --git a/.github/workflows/artifact-release.yml b/.github/workflows/artifact-release.yml new file mode 100644 index 0000000000..92c0418043 --- /dev/null +++ b/.github/workflows/artifact-release.yml @@ -0,0 +1,33 @@ +name: Artifact Release + +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag (v#.#.#)" + type: string + required: true + +permissions: + contents: write + +jobs: + release-dispatch: + if: inputs.tag != '' + uses: opentffoundation/scripts/.github/workflows/release.yml@main + with: + tag: ${{ inputs.tag }} + secrets: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GH_PAT: ${{ secrets.GH_PAT }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + + release-push: + if: inputs.tag == '' + uses: opentffoundation/scripts/.github/workflows/release.yml@main + with: + tag: ${{ github.ref_name }} + secrets: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GH_PAT: ${{ secrets.GH_PAT }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/fork_sync.yml b/.github/workflows/fork_sync.yml new file mode 100644 index 0000000000..a54bc38af4 --- /dev/null +++ b/.github/workflows/fork_sync.yml @@ -0,0 +1,11 @@ +name: Sync Fork + +on: + schedule: + - cron: '15 */4 * * *' # every hour + workflow_dispatch: # on button click + +jobs: + sync: + uses: opentffoundation/scripts/.github/workflows/sync.yml@main + secrets: inherit diff --git a/.github/workflows/go/sign/go.mod b/.github/workflows/go/sign/go.mod new file mode 100644 index 0000000000..5a3410cdc9 --- /dev/null +++ b/.github/workflows/go/sign/go.mod @@ -0,0 +1,18 @@ +module github.com/opentofu/scripts/go/sign + +go 1.21.1 + +require github.com/google/go-github/v54 v54.0.0 + +require ( + github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-querystring v1.1.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/oauth2 v0.11.0 // indirect + golang.org/x/sys v0.11.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/.github/workflows/go/sign/go.sum b/.github/workflows/go/sign/go.sum new file mode 100644 index 0000000000..47e92421f2 --- /dev/null +++ b/.github/workflows/go/sign/go.sum @@ -0,0 +1,46 @@ +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v54 v54.0.0 h1:OZdXwow4EAD5jEo5qg+dGFH2DpkyZvVsAehjvJuUL/c= +github.com/google/go-github/v54 v54.0.0/go.mod h1:Sw1LXWHhXRZtzJ9LI5fyJg9wbQzYvFhW8W5P2yaAQ7s= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/.github/workflows/go/sign/main.go b/.github/workflows/go/sign/main.go new file mode 100644 index 0000000000..31123c711e --- /dev/null +++ b/.github/workflows/go/sign/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/google/go-github/v54/github" +) + +var ( + owner = flag.String("owner", "", "GitHub repo owner name") + repo = flag.String("repo", "", "GitHub repo name") + + fingerprint = flag.String("fingerprint", "", "GPG fingerprint") +) + +var c *github.Client + +func main() { + flag.Parse() + + pat := os.Getenv("GITHUB_PAT") + if pat == "" { + panic("GITHUB_PAT environment variable not set") + } + + c = github.NewTokenClient(nil, pat) + + err := resignReleases(context.Background(), *fingerprint) + if err != nil { + log.Fatalf("failed to resign releases: %v", err) + } +} + +func resignReleases(ctx context.Context, fingerprint string) error { + tmpdir, err := os.MkdirTemp("", "") + if err != nil { + return err + } + defer os.RemoveAll(tmpdir) + log.Printf("tmpdir: %s", tmpdir) + + releases, err := getReleases(ctx, "v") + if err != nil { + return fmt.Errorf("could not list %s/%s releases: %w", *owner, *repo, err) + } + log.Printf("releases: %d", len(releases)) + + for _, release := range releases { + log.Printf("release: %s", release.GetTagName()) + assets, _, err := c.Repositories.ListReleaseAssets(ctx, *owner, *repo, release.GetID(), &github.ListOptions{}) + if err != nil { + return fmt.Errorf("could not list %s/%s release %s assets: %w", *owner, *repo, release.GetTagName(), err) + } + log.Printf("assets: %d", len(assets)) + var ( + checksumAssetID int64 = -1 + checksumAssetName string + signatureAssetID int64 = -1 + ) + for _, asset := range assets { + log.Printf("asset: %s=%d", asset.GetName(), asset.GetID()) + if strings.HasSuffix(asset.GetName(), "_SHA256SUMS") { + checksumAssetID = asset.GetID() + checksumAssetName = asset.GetName() + } + if strings.HasSuffix(asset.GetName(), "_SHA256SUMS.sig") { + signatureAssetID = asset.GetID() + } + } + log.Printf("checksum=%d,signature=%d", checksumAssetID, signatureAssetID) + if checksumAssetID < 0 || signatureAssetID < 0 { + return fmt.Errorf("could not find %s/%s release %s assets, checksum=%t,signature=%t", *owner, *repo, release.GetTagName(), checksumAssetID < 0, signatureAssetID < 0) + } + log.Printf("download asset %d as %s", checksumAssetID, checksumAssetName) + if err := downloadAsset(ctx, checksumAssetID, filepath.Join(tmpdir, checksumAssetName)); err != nil { + return fmt.Errorf("could not download %s/%s release %s checksum asset %d: %w", *owner, *repo, release.GetTagName(), checksumAssetID, err) + } + log.Printf("sign asset %s", checksumAssetName) + signatureFilename, err := sign(fingerprint, tmpdir, checksumAssetName) + if err != nil { + return err + } + log.Printf("delete asset %d", signatureAssetID) + if err := deleteAsset(ctx, signatureAssetID); err != nil { + return fmt.Errorf("could not delete %s/%s release %s asset %d: %w", *owner, *repo, release.GetTagName(), signatureAssetID, err) + } + log.Printf("upload asset %s", signatureFilename) + if err := uploadAsset(ctx, release.GetID(), filepath.Join(tmpdir, signatureFilename), signatureFilename); err != nil { + return fmt.Errorf("could not upload %s/%s release %s asset %s: %w", *owner, *repo, release.GetTagName(), signatureFilename, err) + } + } + + return nil +} + +func getReleases(ctx context.Context, prefix string) ([]*github.RepositoryRelease, error) { + var repoReleases []*github.RepositoryRelease + page := 1 + for { + releases, resp, err := c.Repositories.ListReleases(ctx, *owner, *repo, &github.ListOptions{ + Page: page, + PerPage: 99, + }) + if err != nil { + return nil, err + } + if prefix != "" { + for _, release := range releases { + if strings.HasPrefix(release.GetTagName(), prefix) { + repoReleases = append(repoReleases, release) + } + } + } else { + repoReleases = append(repoReleases, releases...) + } + if resp.NextPage == 0 { + break + } + page = resp.NextPage + } + return repoReleases, nil +} + +func downloadAsset(ctx context.Context, id int64, filename string) error { + rc, _, err := c.Repositories.DownloadReleaseAsset(ctx, *owner, *repo, id, http.DefaultClient) + if err != nil { + return fmt.Errorf("could not download asset: %w", err) + } + defer rc.Close() + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("could not create file %s: %w", filename, err) + } + defer f.Close() + if _, err := io.Copy(f, rc); err != nil { + return fmt.Errorf("could not copy asset data: %w", err) + } + if err := f.Close(); err != nil { + return fmt.Errorf("could not close file %s: %w", filename, err) + } + return nil +} + +func deleteAsset(ctx context.Context, id int64) error { + _, err := c.Repositories.DeleteReleaseAsset(ctx, *owner, *repo, id) + if err != nil { + return fmt.Errorf("could not download asset: %w", err) + } + return nil +} + +func uploadAsset(ctx context.Context, id int64, filename, name string) error { + f, err := os.Open(filename) + if err != nil { + return fmt.Errorf("could not open %s: %w", filename, err) + } + defer f.Close() + _, _, err = c.Repositories.UploadReleaseAsset(ctx, *owner, *repo, id, &github.UploadOptions{Name: name}, f) + if err != nil { + return fmt.Errorf("could not upload asset: %w", err) + } + return nil +} + +func sign(fingerprint string, dir, filename string) (string, error) { + signatureFilename := filename + ".sig" + cmd := exec.Command("gpg", "--batch", "--local-user", fingerprint, "--output", signatureFilename, "--detach-sign", filename) + cmd.Dir = dir + b, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("command failed '%s': %w", string(b), err) + } + return signatureFilename, nil +} diff --git a/.github/workflows/resign.yml b/.github/workflows/resign.yml new file mode 100644 index 0000000000..dae788a427 --- /dev/null +++ b/.github/workflows/resign.yml @@ -0,0 +1,17 @@ +name: Artifacts Resign + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + resign: + uses: opentffoundation/scripts/.github/workflows/sign.yml@main + with: + owner: ${{ github.repository_owner }} + repo: ${{ github.event.repository.name }} + secrets: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GH_PAT: ${{ secrets.GH_PAT }}