From e5ffcc11a56d3b4d38fdbed0ecdb02edc587e7af Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Fri, 15 Aug 2025 11:04:14 -0500 Subject: [PATCH 01/31] fix: add release secrets from vault (#2) Signed-off-by: matttrach --- .github/workflows/release.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf3a6a0..63ad296 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,7 +76,6 @@ jobs: repo: "${{ github.event.repository.name }}", body: "Tests Failed!" }) - # These run after release-please generates a release, so when the release PR is merged - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 if: steps.release-please.outputs.version @@ -87,25 +86,30 @@ jobs: with: go-version-file: 'go.mod' cache: true + - name: retrieve GPG Credentials + uses: rancher-eio/read-vault-secrets@main + with: + secrets: | + secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg passphrase | GPG_PASSPHRASE ; + secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKeyId | GPG_KEY_ID; + secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKey | GPG_KEY; - name: import_gpg_key if: steps.release-please.outputs.version id: import_gpg_key env: - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} - GPG_KEY: ${{ secrets.GPG_KEY }} + GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }} + GPG_KEY_ID: ${{ env.GPG_KEY_ID }} + GPG_KEY: ${{ env.GPG_KEY }} run: | cleanup() { # clear history just in case history -c } trap cleanup EXIT TERM - # sanitize variables if [ -z "${GPG_PASSPHRASE}" ]; then echo "gpg passphrase empty"; exit 1; fi if [ -z "${GPG_KEY_ID}" ]; then echo "key id empty"; exit 1; fi if [ -z "${GPG_KEY}" ]; then echo "key contents empty"; exit 1; fi - echo "Importing gpg key" echo "${GPG_KEY}" | gpg --import --batch > /dev/null || { echo "Failed to import GPG key"; exit 1; } - name: Run GoReleaser @@ -115,5 +119,5 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_KEY_ID: ${{ env.GPG_KEY_ID }} + GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }} From 31c5a03e8f476f3e73215ff4c732e72d185c68d4 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Fri, 15 Aug 2025 11:23:04 -0500 Subject: [PATCH 02/31] fix: move release please to release branches (#4) * fix: move release please to release branches * fix: use one file and get vault secrets --------- Signed-off-by: matttrach --- .aliases | 12 ++------- .github/CODEOWNERS | 2 +- .github/pull_request_template.md | 28 +++++++++++++++----- .github/workflows/release.yml | 44 ++++++++++++++++++++++++++++++-- .goreleaser.yml | 13 +++++----- flake.lock | 6 ++--- 6 files changed, 75 insertions(+), 30 deletions(-) diff --git a/.aliases b/.aliases index d123c5f..819f753 100644 --- a/.aliases +++ b/.aliases @@ -1,17 +1,9 @@ #!/bin/env sh alias gs='git status' alias gd='git diff' +alias gc='git checkout' alias tf='terraform' -alias tfa='if [ -f ssh_key ]; then chmod 600 ssh_key && ssh-add ssh_key; fi; terraform init; terraform apply --auto-approve' +alias tfa='terraform apply --auto-approve' alias tfd='terraform destroy --auto-approve' -alias tfp='terraform init || terraform providers && terraform validate && terraform plan' -alias tfr='terraform destroy --auto-approve;if [ -f ssh_key ]; then chmod 600 ssh_key && ssh-add ssh_key; fi; terraform init; terraform apply --auto-approve' -alias tfl='terraform state list' alias k='kubectl' -alias tt='run_tests' -# expects AGE_ variables to be set, see .variables and .rcs -alias es='encrypt_secrets' # looks in the secret file list and converts the files into encrypted ones, see .functions -alias ds='decrypt_secrets' # looks in the secret file list and converts all the encrtypted files in to unencrypted ones, see .functions -alias ef='encrypt_file' # see .functions -alias cl='clear_local' # clears all of the temporary files from the directory, see .functions alias sc='shell_check' # runs shellcheck -x on all files with a shbang diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2d4f0dc..46efa51 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @rancher/k3s +* @rancher/terraform-maintainers diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 38e8ce7..6a04bd0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1,30 @@ +## Backport + +Is this a cherry-picked backport from the default branch? +If so, please delete all other sections and complete the sentence below: + +Cherry-pick #1236 (main PR) to release/v1 (release branch) +Addresses #1235 (backport issue) for #1234 (main issue) + + ## Related Issue -Fixes # +If this PR will target main, +please complete the below sentence and add labels for each version this should be released to. + +Addresses #1234 (main issue) +This should be backported to release/v0, release/v1 (comma separated list of target release branches) ## Description -In plain English, describe your approach to addressing the issue linked above. For example, if you made a particular design decision, let us know why you chose this path instead of another solution. +Describe your approach to addressing the issue linked above. +For example, if you made a particular design decision, let us know why you chose this path. - -## Rollback Plan +## Testing -- [ ] If a change needs to be reverted, we will roll out an update to the code within 7 days. +Please describe how you verified this change or why testing isn't relevant. -## Changes to Security Controls +## Breaking -Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain. +Does this change alter an interface that users of the provider will need to adjust to? +Will there be any existing configurations broken by this change? diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63ad296..7db03ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,8 @@ name: release on: push: branches: - - main + - release/v0 + - release/v1 permissions: write-all @@ -76,6 +77,44 @@ jobs: repo: "${{ github.event.repository.name }}", body: "Tests Failed!" }) + - name: retrieve GPG Credentials + uses: rancher-eio/read-vault-secrets@main + with: + secrets: | + secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg passphrase | GPG_PASSPHRASE ; + secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKeyId | GPG_KEY_ID; + secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKey | GPG_KEY; + - name: import_gpg_key + if: steps.release-please.outputs.pr && (steps.run-unit-tests.conclusion == 'success') && (steps.run-acc-tests.conclusion == 'success') + env: + GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }} + GPG_KEY_ID: ${{ env.GPG_KEY_ID }} + GPG_KEY: ${{ env.GPG_KEY }} + run: | + cleanup() { + # clear history just in case + history -c + } + trap cleanup EXIT TERM + + # sanitize variables + if [ -z "${GPG_PASSPHRASE}" ]; then echo "gpg passphrase empty"; exit 1; fi + if [ -z "${GPG_KEY_ID}" ]; then echo "key id empty"; exit 1; fi + if [ -z "${GPG_KEY}" ]; then echo "key contents empty"; exit 1; fi + + echo "Importing gpg key" + echo "${GPG_KEY}" | gpg --import --batch > /dev/null || { echo "Failed to import GPG key"; exit 1; } + - name: Run GoReleaser + if: steps.release-please.outputs.pr && (steps.run-unit-tests.conclusion == 'success') && (steps.run-acc-tests.conclusion == 'success') + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 https://github.com/goreleaser/goreleaser-action + with: + args: release --snapshot --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_KEY_ID: ${{ env.GPG_KEY_ID }} + GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }} + + # These run after release-please generates a release, so when the release PR is merged - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 if: steps.release-please.outputs.version @@ -95,7 +134,6 @@ jobs: secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKey | GPG_KEY; - name: import_gpg_key if: steps.release-please.outputs.version - id: import_gpg_key env: GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }} GPG_KEY_ID: ${{ env.GPG_KEY_ID }} @@ -106,10 +144,12 @@ jobs: history -c } trap cleanup EXIT TERM + # sanitize variables if [ -z "${GPG_PASSPHRASE}" ]; then echo "gpg passphrase empty"; exit 1; fi if [ -z "${GPG_KEY_ID}" ]; then echo "key id empty"; exit 1; fi if [ -z "${GPG_KEY}" ]; then echo "key contents empty"; exit 1; fi + echo "Importing gpg key" echo "${GPG_KEY}" | gpg --import --batch > /dev/null || { echo "Failed to import GPG key"; exit 1; } - name: Run GoReleaser diff --git a/.goreleaser.yml b/.goreleaser.yml index e729a12..2ed6f07 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,11 +1,10 @@ # Copyright (c) HashiCorp, Inc. -# Visit https://goreleaser.com for documentation on how to customize this -# behavior. +# https://goreleaser.com for documentation + version: 2 before: hooks: - # this is just an example and not a requirement for provider building/publishing - go mod tidy builds: - env: @@ -25,12 +24,8 @@ builds: - darwin goarch: - amd64 - - '386' - arm - arm64 - ignore: - - goos: darwin - goarch: '386' binary: '{{ .ProjectName }}_v{{ .Version }}' archives: - formats: [ 'zip' ] @@ -55,6 +50,10 @@ signs: - "${signature}" - "--sign" - "${artifact}" +snapshot: + # "snapshot" is the type of release we use for release candidates + # that are generated when a release branch gets a new merge + name_template: "{{ .ProjectName }}_{{ .ShortCommit }}" release: extra_files: - glob: 'terraform-registry-manifest.json' diff --git a/flake.lock b/flake.lock index d57185e..b999921 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755082269, - "narHash": "sha256-Ix7ALeaxv9tW4uBKWeJnaKpYZtZiX4H4Q/MhEmj4XYA=", + "lastModified": 1755113249, + "narHash": "sha256-/bIVS2iP5mixEQWsaiiJ7EGLtk5Id9OehWbmTbzN6kE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d74de548348c46cf25cb1fcc4b74f38103a4590d", + "rev": "e9e0d35e5f735bf3d1e96815272f46fe7083232c", "type": "github" }, "original": { From 3ad0663037a4d1892a576f7181d5c9a8daaa36d8 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 16:20:03 -0500 Subject: [PATCH 03/31] fix: abstract OS file functions (#6) Signed-off-by: matttrach --- docs/resources/local.md | 2 +- internal/provider/file_local_resource.go | 166 ++++++------ internal/provider/file_local_resource_test.go | 242 +++++++++++------- 3 files changed, 228 insertions(+), 182 deletions(-) diff --git a/docs/resources/local.md b/docs/resources/local.md index 6440b17..1bbbd21 100644 --- a/docs/resources/local.md +++ b/docs/resources/local.md @@ -34,7 +34,7 @@ resource "file_local" "example" { - `directory` (String) The directory where the file will be placed, defaults to the current working directory. - `hmac_secret_key` (String, Sensitive) A string used to generate the file identifier, you can pass this value in the environment variable `TF_FILE_HMAC_SECRET_KEY`.The provider will use a hard coded value as the secret key for unprotected files. - `id` (String) Identifier derived from sha256+HMAC hash of file contents. When setting 'protected' to true this argument is required. However, when 'protected' is false then this should be left empty (computed by the provider). -- `mode` (String) The file permissions to assign to the file, defaults to '0600'. +- `permissions` (String) The file permissions to assign to the file, defaults to '0600'. - `protected` (Boolean) Whether or not to fail update or create if the calculated id doesn't match the given id.When this is true, the 'id' field is required and must match what we calculate as the hash at both create and update times.If the 'id' configured doesn't match what we calculate then the provider will error rather than updating or creating the file.When setting this to true, you will need to either set the `TF_FILE_HMAC_SECRET_KEY` environment variable or set the hmac_secret_key argument. ## Import diff --git a/internal/provider/file_local_resource.go b/internal/provider/file_local_resource.go index 1f8b359..c6012fd 100644 --- a/internal/provider/file_local_resource.go +++ b/internal/provider/file_local_resource.go @@ -34,20 +34,73 @@ import ( var _ resource.Resource = &LocalResource{} var _ resource.ResourceWithImportState = &LocalResource{} -// type FileClient struct{} +// An interface for defining custom file managers. +type fileClient interface { + Create(directory string, name string, data string, permissions string) error + // If file isn't found the error message must have err.Error() == "file not found" + Read(directory string, name string) (string, string, error) // permissions, contents, error + Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error + Delete(directory string, name string) error +} -// func (f *FileClient) Create() {} -// func (f *FileClient) Read() {} -// func (f *FileClient) Update() {} -// func (f *FileClient) Delete() {} +// The default fileClient, using the os package. +type osFileClient struct{} + +var _ fileClient = &osFileClient{} // make sure the osFileClient implements the fileClient +func (c *osFileClient) Create(directory string, name string, data string, permissions string) error { + path := filepath.Join(directory, name) + modeInt, err := strconv.ParseUint(permissions, 8, 32) + if err != nil { + return err + } + return os.WriteFile(path, []byte(data), os.FileMode(modeInt)) +} +func (c *osFileClient) Read(directory string, name string) (string, string, error) { + path := filepath.Join(directory, name) + info, err := os.Stat(path) + if err != nil && os.IsNotExist(err) { + return "", "", fmt.Errorf("file not found") + } + if err != nil { + return "", "", err + } + mode := fmt.Sprintf("%#o", info.Mode().Perm()) + contents, err := os.ReadFile(path) + if err != nil { + return "", "", err + } + return mode, string(contents), nil +} +func (c *osFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error { + currentPath := filepath.Join(currentDirectory, currentName) + newPath := filepath.Join(newDirectory, newName) + if currentPath != newPath { + err := os.Rename(currentPath, newPath) + if err != nil { + return err + } + } + modeInt, err := strconv.ParseUint(permissions, 8, 32) + if err != nil { + return err + } + if err = os.WriteFile(newPath, []byte(data), os.FileMode(modeInt)); err != nil { + return err + } + return nil +} +func (c *osFileClient) Delete(directory string, name string) error { + path := filepath.Join(directory, name) + return os.Remove(path) +} func NewLocalResource() resource.Resource { return &LocalResource{} } -// LocalResource defines the resource implementation. -// This facilitates the LocalResource class, it can now be used in functions with *LocalResource. -type LocalResource struct{} +type LocalResource struct { + client fileClient +} // LocalResourceModel describes the resource data model. type LocalResourceModel struct { @@ -55,10 +108,9 @@ type LocalResourceModel struct { Name types.String `tfsdk:"name"` Contents types.String `tfsdk:"contents"` Directory types.String `tfsdk:"directory"` - Mode types.String `tfsdk:"mode"` + Permissions types.String `tfsdk:"permissions"` HmacSecretKey types.String `tfsdk:"hmac_secret_key"` Protected types.Bool `tfsdk:"protected"` - // Fake types.Bool `tfsdk:"fake"` } func (r *LocalResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -84,7 +136,7 @@ func (r *LocalResource) Schema(ctx context.Context, req resource.SchemaRequest, Computed: true, // whenever an argument has a default value it should have Computed: true Default: stringdefault.StaticString("."), }, - "mode": schema.StringAttribute{ + "permissions": schema.StringAttribute{ MarkdownDescription: "The file permissions to assign to the file, defaults to '0600'.", Optional: true, Computed: true, @@ -141,6 +193,10 @@ func (r *LocalResource) Configure(ctx context.Context, req resource.ConfigureReq if req.ProviderData == nil { return } + // Allow the ability to inject a file client, but use the osFileClient by default. + if r.client == nil { + r.client = &osFileClient{} + } } // We should: @@ -161,7 +217,7 @@ func (r *LocalResource) Create(ctx context.Context, req resource.CreateRequest, name := plan.Name.ValueString() directory := plan.Directory.ValueString() contents := plan.Contents.ValueString() - modeString := plan.Mode.ValueString() + permString := plan.Permissions.ValueString() hmacSecretKey := plan.HmacSecretKey.ValueString() protected := plan.Protected.ValueBool() @@ -190,14 +246,8 @@ func (r *LocalResource) Create(ctx context.Context, req resource.CreateRequest, plan.HmacSecretKey = types.StringValue("") } - localFilePath := filepath.Join(directory, name) - modeInt, err := strconv.ParseUint(modeString, 8, 32) - if err != nil { - resp.Diagnostics.AddError("Error reading file mode from config: ", err.Error()) - return - } - if err = os.WriteFile(localFilePath, []byte(contents), os.FileMode(modeInt)); err != nil { - resp.Diagnostics.AddError("Error writing file: ", err.Error()) + if err = r.client.Create(directory, name, contents, permString); err != nil { + resp.Diagnostics.AddError("Error creating file: ", err.Error()) return } @@ -220,28 +270,24 @@ func (r *LocalResource) Read(ctx context.Context, req resource.ReadRequest, resp sName := state.Name.ValueString() sDirectory := state.Directory.ValueString() sContents := state.Contents.ValueString() - sMode := state.Mode.ValueString() + sPerm := state.Permissions.ValueString() sHmacSecretKey := state.HmacSecretKey.ValueString() - sFilePath := filepath.Join(sDirectory, sName) - // If Possible, we should avoid reading the file into memory // The "real" (non-calculated) parts of the file are the path, the contents, and the mode // If the file doesn't exist at the path, then we need to (re)create it - if _, err := os.Stat(sFilePath); os.IsNotExist(err) { + perm, contents, err := r.client.Read(sDirectory, sName) + if err != nil && err.Error() == "File not found." { resp.State.RemoveResource(ctx) return } - - // If the file's contents have changed, then we need to update the state - c, err := os.ReadFile(sFilePath) if err != nil { resp.Diagnostics.AddError("Error reading file: ", err.Error()) return } - contents := string(c) + if contents != sContents { // update state with actual contents state.Contents = types.StringValue(contents) @@ -260,16 +306,9 @@ func (r *LocalResource) Read(ctx context.Context, req resource.ReadRequest, resp state.Id = types.StringValue(id) } - // If the file's mode has changed, then we need to update the state - inf, err := os.Stat(sFilePath) - if err != nil { - resp.Diagnostics.AddError("Error reading file stat: ", err.Error()) - return - } - mode := fmt.Sprintf("%#o", inf.Mode().Perm()) - if mode != sMode { + if perm != sPerm { // update the state with the actual mode - state.Mode = types.StringValue(mode) + state.Permissions = types.StringValue(perm) } resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -292,12 +331,10 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest, cName := config.Name.ValueString() cContents := config.Contents.ValueString() cDirectory := config.Directory.ValueString() - cMode := config.Mode.ValueString() + cPerm := config.Permissions.ValueString() cHmacSecretKey := config.HmacSecretKey.ValueString() cProtected := config.Protected.ValueBool() - cFilePath := filepath.Join(cDirectory, cName) - cKey := cHmacSecretKey if cKey == "" { cKey = os.Getenv("TF_FILE_HMAC_SECRET_KEY") @@ -318,6 +355,7 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest, config.HmacSecretKey = types.StringValue("") } + // Read updates state with reality, so state = reality var reality LocalResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &reality)...) if resp.Diagnostics.HasError() { @@ -325,47 +363,13 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest, } rName := reality.Name.ValueString() - rContents := reality.Contents.ValueString() rDirectory := reality.Directory.ValueString() - rMode := reality.Mode.ValueString() - - rFilePath := filepath.Join(rDirectory, rName) - - if rFilePath != cFilePath { - // config is changing the file path, we need to move the file - err := os.Rename(rFilePath, cFilePath) - if err != nil { - resp.Diagnostics.AddError("Error moving file: ", err.Error()) - return - } - } // the config's file path (cFilePath) is now accurate - - if rMode != cMode { - // the config is changing the mode - modeInt, err := strconv.ParseUint(cMode, 8, 32) - if err != nil { - resp.Diagnostics.AddError("Error reading file mode from config: ", err.Error()) - return - } - err = os.Chmod(cFilePath, os.FileMode(modeInt)) - if err != nil { - resp.Diagnostics.AddError("Error changing file mode: ", err.Error()) - return - } - } // the config's mode (cMode) is now accurate - if cContents != rContents { - // config is changing the contents - modeInt, err := strconv.ParseUint(cMode, 8, 32) - if err != nil { - resp.Diagnostics.AddError("Error reading file mode from config: ", err.Error()) - return - } - if err = os.WriteFile(cFilePath, []byte(cContents), os.FileMode(modeInt)); err != nil { - resp.Diagnostics.AddError("Error writing file: ", err.Error()) - return - } - } // the config's contents (cContents) are now accurate + err := r.client.Update(rDirectory, rName, cDirectory, cName, cContents, cPerm) + if err != nil { + resp.Diagnostics.AddError("Error updating file: ", err.Error()) + return + } // the path, mode, and contents are all of the "real" parts of the file // the id is calculated from the secret key and contents, @@ -396,8 +400,6 @@ func (r *LocalResource) Delete(ctx context.Context, req resource.DeleteRequest, } contents := state.Contents.ValueString() - localFilePath := filepath.Join(directory, name) - // we need to validate the id before we can delete a protected file if protected { err := validateProtected(protected, id, key, contents) @@ -407,7 +409,7 @@ func (r *LocalResource) Delete(ctx context.Context, req resource.DeleteRequest, } } - if err := os.Remove(localFilePath); err != nil { + if err := r.client.Delete(directory, name); err != nil { tflog.Error(ctx, "Failed to delete file: "+err.Error()) return } diff --git a/internal/provider/file_local_resource_test.go b/internal/provider/file_local_resource_test.go index 70df3a5..a6e89d0 100644 --- a/internal/provider/file_local_resource_test.go +++ b/internal/provider/file_local_resource_test.go @@ -4,8 +4,7 @@ package provider import ( "context" - "os" - "path/filepath" + "fmt" "slices" "strconv" "testing" @@ -19,7 +18,7 @@ import ( const ( defaultId = "" defaultDirectory = "." - defaultMode = "0600" + defaultPerm = "0600" defaultProtected = "false" defaultHmacSecretKey = "" ) @@ -73,21 +72,20 @@ func TestLocalSchema(t *testing.T) { func TestLocalResourceCreate(t *testing.T) { t.Run("Create function", func(t *testing.T) { testCases := []struct { - name string - fit LocalResource - have resource.CreateRequest - want resource.CreateResponse - tearDownPath string + name string + fit LocalResource + have resource.CreateRequest + want resource.CreateResponse }{ { "Basic", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getCreateRequest(t, map[string]string{ "id": defaultId, "name": "test_basic.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a basic test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, // this should use the hard coded hmac secret key for unprotected files @@ -97,22 +95,21 @@ func TestLocalResourceCreate(t *testing.T) { "id": "3de642fb91d2fb0ce02fe66c3d19ebdf44cbc6a2ebcc2dad22f1950b67c1217f", "name": "test_basic.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a basic test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, }), - filepath.Join(defaultDirectory, "test_basic.tmp"), }, { "Protected", - LocalResource{}, + LocalResource{client: &osFileClient{}}, // have getCreateRequest(t, map[string]string{ "id": "4ccd8ec7ea24e0524c8aba459fbf3a2649ec3cd96a1c8f9dfb326cc57a9d3127", "name": "test_protected.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a test", "protected": "true", "hmac_secret_key": "this-is-a-test-key", @@ -122,22 +119,21 @@ func TestLocalResourceCreate(t *testing.T) { "id": "4ccd8ec7ea24e0524c8aba459fbf3a2649ec3cd96a1c8f9dfb326cc57a9d3127", "name": "test_protected.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a test", "protected": "true", "hmac_secret_key": "this-is-a-test-key", }), - filepath.Join(defaultDirectory, "test_protected.tmp"), }, { "Protected using key from environment", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getCreateRequest(t, map[string]string{ "id": "59fed8691a76c7693fc9dcd4fda28390a1fd3090114bc64f3e5a3abe312a92f5", "name": "test_protected.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a test", "protected": "true", "hmac_secret_key": defaultHmacSecretKey, // this relies on TF_FILE_HMAC_SECRET_KEY=thisisasupersecretkey in your environment @@ -147,12 +143,11 @@ func TestLocalResourceCreate(t *testing.T) { "id": "59fed8691a76c7693fc9dcd4fda28390a1fd3090114bc64f3e5a3abe312a92f5", "name": "test_protected.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a test", "protected": "true", "hmac_secret_key": defaultHmacSecretKey, }), - filepath.Join(defaultDirectory, "test_protected.tmp"), }, } for _, tc := range testCases { @@ -163,12 +158,18 @@ func TestLocalResourceCreate(t *testing.T) { } plannedProtected := plannedState.Protected.ValueBool() plannedHmacSecretKey := plannedState.HmacSecretKey.ValueString() + plannedDirectory := plannedState.Directory.ValueString() + plannedName := plannedState.Name.ValueString() if plannedProtected && plannedHmacSecretKey == "" { t.Setenv("TF_FILE_HMAC_SECRET_KEY", "thisisasupersecretkey") } r := getCreateResponseContainer() tc.fit.Create(context.Background(), tc.have, &r) - defer teardown(tc.tearDownPath) + defer func() { + if err := tc.fit.client.Delete(plannedDirectory, plannedName); err != nil { + t.Errorf("Error cleaning up: %v", err) + } + }() got := r if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("Create() mismatch (-want +got):\n%s", diff) @@ -181,22 +182,21 @@ func TestLocalResourceCreate(t *testing.T) { func TestLocalResourceRead(t *testing.T) { t.Run("Read function", func(t *testing.T) { testCases := []struct { - name string - fit LocalResource - have resource.ReadRequest - want resource.ReadResponse - setup map[string]string - tearDownPath string + name string + fit LocalResource + have resource.ReadRequest + want resource.ReadResponse + setup map[string]string }{ { "Unprotected", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getReadRequest(t, map[string]string{ "id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2", "name": "read.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is an unprotected read test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, @@ -206,27 +206,27 @@ func TestLocalResourceRead(t *testing.T) { "id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2", "name": "read.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is an unprotected read test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, }), map[string]string{ - "mode": defaultMode, - "path": filepath.Join(defaultDirectory, "read.tmp"), - "contents": "this is an unprotected read test", + "mode": defaultPerm, + "directory": defaultDirectory, + "name": "read.tmp", + "contents": "this is an unprotected read test", }, - filepath.Join(defaultDirectory, "read.tmp"), }, { "Protected", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getReadRequest(t, map[string]string{ "id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1", "name": "read_protected.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a protected read test", "protected": "true", "hmac_secret_key": "this-is-a-test-key", @@ -236,28 +236,28 @@ func TestLocalResourceRead(t *testing.T) { "id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1", "name": "read_protected.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a protected read test", "protected": "true", "hmac_secret_key": "this-is-a-test-key", }), // reality map[string]string{ - "mode": defaultMode, - "path": filepath.Join(defaultDirectory, "read_protected.tmp"), - "contents": "this is a protected read test", + "mode": defaultPerm, + "directory": defaultDirectory, + "name": "read_protected.tmp", + "contents": "this is a protected read test", }, - filepath.Join(defaultDirectory, "read_protected.tmp"), }, { "Protected with content update", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getReadRequest(t, map[string]string{ "id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1", "name": "read_protected_content.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a protected read test", "protected": "true", "hmac_secret_key": "this-is-a-test-key", @@ -267,28 +267,28 @@ func TestLocalResourceRead(t *testing.T) { "id": "84326116e261654e44ca3cb73fa026580853794062d472bc817b7ec2c82ff648", "name": "read_protected_content.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a change in contents in the real file", "protected": "true", "hmac_secret_key": "this-is-a-test-key", }), // reality map[string]string{ - "mode": defaultMode, - "path": filepath.Join(defaultDirectory, "read_protected_content.tmp"), - "contents": "this is a change in contents in the real file", + "mode": defaultPerm, + "directory": defaultDirectory, + "name": "read_protected_content.tmp", + "contents": "this is a change in contents in the real file", }, - filepath.Join(defaultDirectory, "read_protected_content.tmp"), }, { "Protected with mode update", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getReadRequest(t, map[string]string{ "id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1", "name": "read_protected_mode.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a protected read test", "protected": "true", "hmac_secret_key": "this-is-a-test-key", @@ -298,24 +298,30 @@ func TestLocalResourceRead(t *testing.T) { "id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1", "name": "read_protected_mode.tmp", "directory": defaultDirectory, - "mode": "0755", + "permissions": "0755", "contents": "this is a protected read test", "protected": "true", "hmac_secret_key": "this-is-a-test-key", }), // reality map[string]string{ - "mode": "0755", - "path": filepath.Join(defaultDirectory, "read_protected_mode.tmp"), - "contents": "this is a protected read test", + "mode": "0755", + "directory": defaultDirectory, + "name": "read_protected_mode.tmp", + "contents": "this is a protected read test", }, - filepath.Join(defaultDirectory, "read_protected_mode.tmp"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - setup(tc.setup) - defer teardown(tc.tearDownPath) + if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], tc.setup["mode"]); err != nil { + t.Errorf("Error setting up: %v", err) + } + defer func() { + if err := tc.fit.client.Delete(tc.setup["directory"], tc.setup["name"]); err != nil { + t.Errorf("Error tearing down: %v", err) + } + }() r := getReadResponseContainer() tc.fit.Read(context.Background(), tc.have, &r) got := r @@ -326,27 +332,25 @@ func TestLocalResourceRead(t *testing.T) { } }) } - func TestLocalResourceUpdate(t *testing.T) { t.Run("Update function", func(t *testing.T) { testCases := []struct { - name string - fit LocalResource - have resource.UpdateRequest - want resource.UpdateResponse - setup map[string]string - tearDownPath string + name string + fit LocalResource + have resource.UpdateRequest + want resource.UpdateResponse + setup map[string]string }{ { "Basic test", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getUpdateRequest(t, map[string]map[string]string{ "priorState": { "id": defaultId, "name": "update_basic.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is an update test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, @@ -355,7 +359,7 @@ func TestLocalResourceUpdate(t *testing.T) { "id": defaultId, "name": "update_basic.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a basic update test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, @@ -366,24 +370,30 @@ func TestLocalResourceUpdate(t *testing.T) { "id": "0ec41eee6c157a3f7e50b78d586ee2ddb4d6e93b6de8bdf6d9354cf720e89549", "name": "update_basic.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a basic update test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, }), // setup map[string]string{ - "mode": defaultMode, - "path": filepath.Join(defaultDirectory, "update_basic.tmp"), - "contents": "this is an update test", + "mode": defaultPerm, + "directory": defaultDirectory, + "name": "update_basic.tmp", + "contents": "this is an update test", }, - filepath.Join(defaultDirectory, "update_basic.tmp"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - setup(tc.setup) - defer teardown(tc.tearDownPath) + if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], tc.setup["mode"]); err != nil { + t.Errorf("Error setting up: %v", err) + } + defer func() { + if err := tc.fit.client.Delete(tc.setup["directory"], tc.setup["name"]); err != nil { + t.Errorf("Error tearing down: %v", err) + } + }() r := getUpdateResponseContainer() tc.fit.Update(context.Background(), tc.have, &r) got := r @@ -392,13 +402,12 @@ func TestLocalResourceUpdate(t *testing.T) { t.Errorf("Failed to get planned state: %v", diags) } plannedContents := plannedState.Contents.ValueString() - plannedFilePath := filepath.Join(plannedState.Directory.ValueString(), plannedState.Name.ValueString()) - contentsAfterUpdate, err := os.ReadFile(plannedFilePath) + _, contentsAfterUpdate, err := tc.fit.client.Read(plannedState.Directory.ValueString(), plannedState.Name.ValueString()) if err != nil { t.Errorf("Failed to read file for update verification: %s", err) } - if string(contentsAfterUpdate) != plannedContents { - t.Errorf("File content was not updated correctly. Got %q, want %q", string(contentsAfterUpdate), plannedContents) + if contentsAfterUpdate != plannedContents { + t.Errorf("File content was not updated correctly. Got %q, want %q", contentsAfterUpdate, plannedContents) } if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("Update() mismatch (-want +got):\n%s", diff) @@ -411,22 +420,21 @@ func TestLocalResourceUpdate(t *testing.T) { func TestLocalResourceDelete(t *testing.T) { t.Run("Delete function", func(t *testing.T) { testCases := []struct { - name string - fit LocalResource - have resource.DeleteRequest - want resource.DeleteResponse - setup map[string]string - tearDownPath string + name string + fit LocalResource + have resource.DeleteRequest + want resource.DeleteResponse + setup map[string]string }{ { "Basic test", - LocalResource{}, + LocalResource{client: &memoryFileClient{}}, // have getDeleteRequest(t, map[string]string{ "id": "fd6fb8621c4850c228190f4d448ce30881a32609d6b4c7341d48d0027e597567", "name": "delete.tmp", "directory": defaultDirectory, - "mode": defaultMode, + "permissions": defaultPerm, "contents": "this is a delete test", "protected": defaultProtected, "hmac_secret_key": defaultHmacSecretKey, @@ -435,22 +443,27 @@ func TestLocalResourceDelete(t *testing.T) { getDeleteResponse(), // setup map[string]string{ - "mode": defaultMode, - "path": filepath.Join(defaultDirectory, "delete.tmp"), - "contents": "this is a delete test", + "mode": defaultPerm, + "directory": defaultDirectory, + "name": "delete.tmp", + "contents": "this is a delete test", }, - filepath.Join(defaultDirectory, "delete.tmp"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - setup(tc.setup) + if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], tc.setup["mode"]); err != nil { + t.Errorf("Error setting up: %v", err) + } r := getDeleteResponseContainer() tc.fit.Delete(context.Background(), tc.have, &r) got := r // Verify the file was actually deleted from disk - if _, err := os.Stat(tc.setup["path"]); !os.IsNotExist(err) { - t.Errorf("Expected file to be deleted, but it still exists.") + if _, c, err := tc.fit.client.Read(tc.setup["directory"], tc.setup["name"]); err == nil || err.Error() != "file not found" { + if err == nil { + t.Errorf("Expected file to be delete, but it still exists. File contents: %s", c) + } + t.Errorf("Expected file to be deleted, but it still exists. Error: %s", err.Error()) } // verify that the file was removed from state if diff := cmp.Diff(tc.want, got); diff != "" { @@ -682,7 +695,7 @@ func getObjectAttributeTypes() tftypes.Object { "id": tftypes.String, "name": tftypes.String, "directory": tftypes.String, - "mode": tftypes.String, + "permissions": tftypes.String, "contents": tftypes.String, "hmac_secret_key": tftypes.String, "protected": tftypes.Bool, @@ -697,11 +710,42 @@ func getLocalResourceSchema() *resource.SchemaResponse { return r } -func setup(data map[string]string) { - modeInt, _ := strconv.ParseUint(data["mode"], 8, 32) - _ = os.WriteFile(data["path"], []byte(data["contents"]), os.FileMode(modeInt)) +// type fileClient interface { +// Create(directory string, name string, data string, permissions string) error +// // If file isn't found the error message must have err.Error() == "File not found." +// Read(directory string, name string) (string, string, error)// permissions, contents, error +// Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error +// Delete(directory string, name string) error +// } + +type memoryFileClient struct { + file map[string]string } -func teardown(path string) { - os.Remove(path) +var _ fileClient = &memoryFileClient{} // make sure the memoryFileClient implements the fileClient +func (c *memoryFileClient) Create(directory string, name string, data string, permissions string) error { + + c.file = make(map[string]string) + c.file["directory"] = directory + c.file["name"] = name + c.file["contents"] = data + c.file["permissions"] = permissions + return nil +} +func (c *memoryFileClient) Read(directory string, name string) (string, string, error) { + if c.file["directory"] == "" || c.file["name"] == "" { + return "", "", fmt.Errorf("file not found") + } + return c.file["permissions"], c.file["contents"], nil +} +func (c *memoryFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error { + c.file["directory"] = newDirectory + c.file["name"] = newName + c.file["contents"] = data + c.file["permissions"] = permissions + return nil +} +func (c *memoryFileClient) Delete(directory string, name string) error { + c.file = nil + return nil } From 5f092ac3528b11da66e52ebaa05783f7d4967544 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 18:12:44 -0500 Subject: [PATCH 04/31] fix: add automation to generate sub issues (#7) Signed-off-by: matttrach --- .github/workflows/backport.yml | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000..8dc822e --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,50 @@ +name: Backports + +on: + issues: + types: [labeled] # triggered when any label is added to an issue + +jobs: + create-issue: + runs-on: ubuntu-latest + if: ${{ github.event.label.name == 'version/v0' }} + steps: + - name: Create GitHub Issue + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const parentIssueNumber = context.payload.issue.number; + + // Fetch repository and parent issue details + const { repository } = await github.graphql(` + query($owner: String!, $repo: String!, $issueNumber: Int!) { + repository(owner: $owner, name: $repo) { + id + issue(number: $issueNumber) { + id + } + } + } + `, { owner, repo, issueNumber: parentIssueNumber }); + + const repositoryId = repository.id; + + // Create the sub-issue + const { createIssue } = await github.graphql(` + mutation($repositoryId: ID!, $title: String!, $body: String!) { + createIssue(input: {repositoryId: $repositoryId, title: $title, body: $body}) { + issue { + id + number + } + } + } + `, { + repositoryId, + title: "Backport #" + parentIssueNumber + " to release/v0", + body: "Backport #" + parentIssueNumber + " to release/v0" + }); + + // const subIssueId = createIssue.issue.id; + // const subIssueNumber = createIssue.issue.number; From 814c480d602f25cdf7f4e14e27a8344d8a245e0b Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 19:12:07 -0500 Subject: [PATCH 05/31] fix: use new path to attach sub issue (#9) Signed-off-by: matttrach --- .github/workflows/backport.yml | 48 ++++++++++++---------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 8dc822e..82b8171 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -13,38 +13,24 @@ jobs: uses: actions/github-script@v7 with: script: | - const { owner, repo } = context.repo; - const parentIssueNumber = context.payload.issue.number; - - // Fetch repository and parent issue details - const { repository } = await github.graphql(` - query($owner: String!, $repo: String!, $issueNumber: Int!) { - repository(owner: $owner, name: $repo) { - id - issue(number: $issueNumber) { - id - } - } - } - `, { owner, repo, issueNumber: parentIssueNumber }); - - const repositoryId = repository.id; + const parentIssue = context.issue; + const parentIssueTitle = context.payload.issue.title; + const parentIssueNumber = parentIssue.number; // Create the sub-issue - const { createIssue } = await github.graphql(` - mutation($repositoryId: ID!, $title: String!, $body: String!) { - createIssue(input: {repositoryId: $repositoryId, title: $title, body: $body}) { - issue { - id - number - } - } - } - `, { - repositoryId, - title: "Backport #" + parentIssueNumber + " to release/v0", - body: "Backport #" + parentIssueNumber + " to release/v0" + const newIssue = await github.rest.issues.create({ + owner: parentIssue.owner, + repo: parentIssue.repo, + title: `Backport #" + parentIssueNumber + " to release/v0`, + body: `"Backport #" + parentIssueNumber + " to release/v0` }); - // const subIssueId = createIssue.issue.id; - // const subIssueNumber = createIssue.issue.number; + const subIssueId = newIssue.data.id; + + // Attach the sub-issue to the parent + await github.rest.issues.addSubIssue({ + owner: parentIssue.owner, + repo: parentIssue.repo, + issue_number: parentIssueNumber, + sub_issue_id: subIssueId + }); From 7aeb11cd143e63a15971df2b4bf1f1b32d979b77 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 21:29:07 -0500 Subject: [PATCH 06/31] fix: use the API endpoint to attach the sub issue (#11) Signed-off-by: matttrach --- .github/workflows/backport.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 82b8171..ff25a6d 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -16,21 +16,26 @@ jobs: const parentIssue = context.issue; const parentIssueTitle = context.payload.issue.title; const parentIssueNumber = parentIssue.number; + const repo = parentIssue.repo; + const owner = parentIssue.owner; // Create the sub-issue const newIssue = await github.rest.issues.create({ - owner: parentIssue.owner, - repo: parentIssue.repo, + owner: owner, + repo: repo, title: `Backport #" + parentIssueNumber + " to release/v0`, body: `"Backport #" + parentIssueNumber + " to release/v0` }); const subIssueId = newIssue.data.id; - // Attach the sub-issue to the parent - await github.rest.issues.addSubIssue({ - owner: parentIssue.owner, - repo: parentIssue.repo, + // Attach the sub-issue to the parent using API request + await github.request('POST /repos/{owner}/{repo}/issues/{parentIssueNumber}/sub_issues', { + owner: owner, + repo: repo, issue_number: parentIssueNumber, - sub_issue_id: subIssueId + sub_issue_id: subIssueId, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } }); From b22c62a00d0ebb2292a36f3b49b58e71732b86e7 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 22:06:12 -0500 Subject: [PATCH 07/31] fix: add console line to see context (#13) Signed-off-by: matttrach --- .github/workflows/backport.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index ff25a6d..c4f202a 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -13,6 +13,8 @@ jobs: uses: actions/github-script@v7 with: script: | + console.log("The full event context:", JSON.stringify(context, null, 2)); + const parentIssue = context.issue; const parentIssueTitle = context.payload.issue.title; const parentIssueNumber = parentIssue.number; @@ -23,8 +25,8 @@ jobs: const newIssue = await github.rest.issues.create({ owner: owner, repo: repo, - title: `Backport #" + parentIssueNumber + " to release/v0`, - body: `"Backport #" + parentIssueNumber + " to release/v0` + title: "Backport #" + parentIssueNumber + " to release/v0", + body: "Backport #" + parentIssueNumber + " to release/v0" }); const subIssueId = newIssue.data.id; From 7a2ebd955e0166cc4ca3ec285aa904989cc43948 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 22:55:22 -0500 Subject: [PATCH 08/31] fix: use the full payload issue (#15) Signed-off-by: matttrach --- .github/workflows/backport.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index c4f202a..b210605 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -13,10 +13,8 @@ jobs: uses: actions/github-script@v7 with: script: | - console.log("The full event context:", JSON.stringify(context, null, 2)); - - const parentIssue = context.issue; - const parentIssueTitle = context.payload.issue.title; + const parentIssue = context.payload.issue; + const parentIssueTitle = parentIssue.title; const parentIssueNumber = parentIssue.number; const repo = parentIssue.repo; const owner = parentIssue.owner; From 55f8f9aee6a515d0baeb23c76fe3719c8a4c8587 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 23:13:26 -0500 Subject: [PATCH 09/31] fix: use a different context (#16) Signed-off-by: matttrach --- .github/workflows/backport.yml | 4 ++-- aspell_custom.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index b210605..b66c8d4 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -16,8 +16,8 @@ jobs: const parentIssue = context.payload.issue; const parentIssueTitle = parentIssue.title; const parentIssueNumber = parentIssue.number; - const repo = parentIssue.repo; - const owner = parentIssue.owner; + const repo = context.repo.repo; + const owner = context.repo.owner; // Create the sub-issue const newIssue = await github.rest.issues.create({ diff --git a/aspell_custom.txt b/aspell_custom.txt index 1c3f9ca..ef203ef 100644 --- a/aspell_custom.txt +++ b/aspell_custom.txt @@ -10,3 +10,4 @@ goreleaser terraform tflint gorelease +repo From 3d6c9eb5bbfd3dcbd66023ac16d02b5edf8df556 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Mon, 18 Aug 2025 23:24:29 -0500 Subject: [PATCH 10/31] fix: use the proper variable name (#18) Signed-off-by: matttrach --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index b66c8d4..22d3603 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -30,7 +30,7 @@ jobs: const subIssueId = newIssue.data.id; // Attach the sub-issue to the parent using API request - await github.request('POST /repos/{owner}/{repo}/issues/{parentIssueNumber}/sub_issues', { + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner: owner, repo: repo, issue_number: parentIssueNumber, From 241b72e742810a3eecb26d0a8620c83e79686901 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 11:29:16 -0500 Subject: [PATCH 11/31] fix: create issue when a pull request hits main (#20) Signed-off-by: matttrach --- .github/workflows/backport.yml | 2 +- .github/workflows/issue-comment-triage.yml | 21 --------------- .github/workflows/main-issue.yml | 30 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 22 deletions(-) delete mode 100644 .github/workflows/issue-comment-triage.yml create mode 100644 .github/workflows/main-issue.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 22d3603..35085c3 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,5 +1,5 @@ name: Backports - +# This workflow generates "backport" issues when a release branch label is added to an issue on: issues: types: [labeled] # triggered when any label is added to an issue diff --git a/.github/workflows/issue-comment-triage.yml b/.github/workflows/issue-comment-triage.yml deleted file mode 100644 index 00017cd..0000000 --- a/.github/workflows/issue-comment-triage.yml +++ /dev/null @@ -1,21 +0,0 @@ -# DO NOT EDIT - This GitHub Workflow is managed by automation -# https://github.com/hashicorp/terraform-devex-repos -name: Issue Comment Triage - -on: - issue_comment: - types: [created] - -jobs: - issue_comment_triage: - runs-on: ubuntu-latest - env: - # issue_comment events are triggered by comments on issues and pull requests. Checking the - # value of github.event.issue.pull_request tells us whether the issue is an issue or is - # actually a pull request, allowing us to dynamically set the gh subcommand: - # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only - COMMAND: ${{ github.event.issue.pull_request && 'pr' || 'issue' }} - GH_TOKEN: ${{ github.token }} - steps: - - name: 'Remove waiting-response on comment' - run: gh ${{ env.COMMAND }} edit ${{ github.event.issue.html_url }} --remove-label waiting-response diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml new file mode 100644 index 0000000..aa12788 --- /dev/null +++ b/.github/workflows/main-issue.yml @@ -0,0 +1,30 @@ +name: MainIssue +# This workflow generates a "main" issue when a PR is created targeting main. +on: + pull_request: + branches: + - main + +jobs: + generate-issue: + name: 'Create Main Issue' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const repo = context.repo.repo; + const owner = context.repo.owner; + const pr = context.payload.pull_request; + + // Create the main issue + const newIssue = await github.rest.issues.create({ + owner: owner, + repo: repo, + title: pr.title, + body: "This is the main issue tracking #" + pr.number + "\n\n" + + "Please add labels indicating the release versions eg. 'version/v0'\n" + + "Please add comments for user issues which this issue addresses. \n" + + "Description copied from PR: \n" + pr.body, + labels: ['internal/main'] + }); From 6c1009008dd1bf0539d2ac6d9340cc608de25054 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 12:03:19 -0500 Subject: [PATCH 12/31] fix: give issue write permissions (#21) * fix: give issue write permissions * fix: try pull request target --------- Signed-off-by: matttrach --- .github/workflows/main-issue.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index aa12788..3c7ce50 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -1,14 +1,16 @@ name: MainIssue # This workflow generates a "main" issue when a PR is created targeting main. on: - pull_request: - branches: - - main + pull_request_target: + branches: [main] + types: [opened] jobs: generate-issue: name: 'Create Main Issue' runs-on: ubuntu-latest + permissions: + issues: write steps: - uses: actions/github-script@v7 with: @@ -22,8 +24,8 @@ jobs: owner: owner, repo: repo, title: pr.title, - body: "This is the main issue tracking #" + pr.number + "\n\n" + - "Please add labels indicating the release versions eg. 'version/v0'\n" + + body: "This is the main issue tracking #" + pr.number + " \n\n" + + "Please add labels indicating the release versions eg. 'version/v0' \n" + "Please add comments for user issues which this issue addresses. \n" + "Description copied from PR: \n" + pr.body, labels: ['internal/main'] From 96b5e8c36fafd31c67d29b99ec25662d42b02798 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 12:17:05 -0500 Subject: [PATCH 13/31] fix: update pull request template (#22) Signed-off-by: matttrach --- .github/pull_request_template.md | 28 ++++++++++++++-------------- aspell_custom.txt | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6a04bd0..23c2361 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,24 +1,16 @@ -## Backport - -Is this a cherry-picked backport from the default branch? -If so, please delete all other sections and complete the sentence below: - -Cherry-pick #1236 (main PR) to release/v1 (release branch) -Addresses #1235 (backport issue) for #1234 (main issue) - - ## Related Issue -If this PR will target main, -please complete the below sentence and add labels for each version this should be released to. +If this PR will target main, please complete the below sentence. Addresses #1234 (main issue) -This should be backported to release/v0, release/v1 (comma separated list of target release branches) + +## Releases + +If this PR should be released, please add labels for each release branch it targets. ## Description -Describe your approach to addressing the issue linked above. -For example, if you made a particular design decision, let us know why you chose this path. +Describe your change and how it addresses the issue linked above. ## Testing @@ -28,3 +20,11 @@ Please describe how you verified this change or why testing isn't relevant. Does this change alter an interface that users of the provider will need to adjust to? Will there be any existing configurations broken by this change? + +## Backport + +Is this a cherry-picked backport from the default branch? +If so, please delete all other sections and complete the sentence below: + +Cherry-pick #1236 (main PR) to release/v1 (release branch) +Addresses #1235 (backport issue) for #1234 (main issue) diff --git a/aspell_custom.txt b/aspell_custom.txt index ef203ef..f8d4fb8 100644 --- a/aspell_custom.txt +++ b/aspell_custom.txt @@ -11,3 +11,4 @@ terraform tflint gorelease repo +pr From ade5addd2bc38b9694aa1a873cea1db8305d8245 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 12:50:21 -0500 Subject: [PATCH 14/31] fix: assign users to main pr (#25) Signed-off-by: matttrach --- .github/workflows/main-issue.yml | 8 +++++--- aspell_custom.txt | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index 3c7ce50..c760c03 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -18,6 +18,7 @@ jobs: const repo = context.repo.repo; const owner = context.repo.owner; const pr = context.payload.pull_request; + const reviewers = pr.requested_reviewers.map(reviewer => reviewer.login) // Create the main issue const newIssue = await github.rest.issues.create({ @@ -25,8 +26,9 @@ jobs: repo: repo, title: pr.title, body: "This is the main issue tracking #" + pr.number + " \n\n" + - "Please add labels indicating the release versions eg. 'version/v0' \n" + - "Please add comments for user issues which this issue addresses. \n" + + "Please add labels indicating the release versions eg. 'version/v0' \n\n" + + "Please add comments for user issues which this issue addresses. \n\n" + "Description copied from PR: \n" + pr.body, - labels: ['internal/main'] + labels: ['internal/main'], + assignees: reviewers }); diff --git a/aspell_custom.txt b/aspell_custom.txt index f8d4fb8..264427b 100644 --- a/aspell_custom.txt +++ b/aspell_custom.txt @@ -12,3 +12,4 @@ tflint gorelease repo pr +assignees From f11e0f2d0f56e583315447695c15c4159893a134 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 19:57:48 -0500 Subject: [PATCH 15/31] fix: add back port pr (#27) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 106 +++++++++++++++++++++++++++++ .github/workflows/backport.yml | 1 + aspell_custom.txt | 1 + 3 files changed, 108 insertions(+) create mode 100644 .github/workflows/backport-prs.yml diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml new file mode 100644 index 0000000..a0b2616 --- /dev/null +++ b/.github/workflows/backport-prs.yml @@ -0,0 +1,106 @@ +name: 'Auto Cherry-Pick to Release Branches' + +on: + push: + branches: + - 'main' + +jobs: + create-cherry-pick-prs: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + actions: read + + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 'Find Issues and Create Cherry-Pick PRs' + uses: actions/github-script@v7 + with: + script: | + const execSync = require('child_process').execSync; + const owner = context.repo.owner; + const repo = context.repo.repo; + const mergeCommitSha = context.payload.head_commit.id; + + const { data: associatedPrs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner, + repo, + commit_sha: mergeCommitSha, + }); + + const pr = associatedPrs.find(p => p.base.ref === 'main' && p.merged_at); + if (!pr) { + core.info(`No merged PR found for commit ${mergeCommitSha}. This may have been a direct push. Exiting.`); + return; + } + core.info(`Found associated PR: #${pr.number}`); + + core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`); + const searchResults = await github.rest.search.issuesAndPullRequests({ + q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}` + }); + + if (searchResults.data.items.length === 0) { + core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); + return; + } + const mainIssue = searchResults.data.items[0]; + core.info(`Found main issue: #${mainIssue.number}`); + + core.info(`Fetching sub-issues for main issue #${mainIssue.number}`); + const { data: subIssues } = await github.rest.issues.listSubIssues({ + owner, + repo, + issue_number: mainIssue.number, + }); + + if (subIssues.length === 0) { + core.info(`No sub-issues found for issue #${mainIssue.number}. Exiting.`); + return; + } + core.info(`Found ${subIssues.length} sub-issues.`); + + for (const subIssue of subIssues) { + try { + const subIssueNumber = subIssue.number; + + // Find the release label directly on the sub-issue object + const releaseLabel = subIssue.labels.find(label => label.name.startsWith('release/v')); + if (!releaseLabel) { + core.warning(`Sub-issue #${subIssueNumber} has no 'release/v...' label. Skipping.`); + continue; + } + const targetBranch = releaseLabel.name + core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`); + + const newBranchName = `backport-${pr.number}-${targetBranch.replace(/\//g, '-')}`; + execSync(`git config user.name "github-actions[bot]"`); + execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`); + execSync(`git fetch origin ${targetBranch}`); + execSync(`git checkout -b ${newBranchName} origin/${targetBranch}`); + + core.info(`Cherry-picking commit ${mergeCommitSha}...`); + execSync(`git cherry-pick -x ${mergeCommitSha}`); + + core.info(`Pushing new branch ${newBranchName}...`); + execSync(`git push origin ${newBranchName}`); + + core.info(`Creating pull request for branch ${newBranchName} targeting ${targetBranch}...`); + + const { data: newPr } = await github.rest.pulls.create({ + owner, + repo, + title: pr.title, + head: newBranchName, + base: targetBranch, + body: "This pull request cherry-picks the changes from #" + pr.number + " into " + targetBranch, + }); + } + } diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 35085c3..270cc0a 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,6 +25,7 @@ jobs: repo: repo, title: "Backport #" + parentIssueNumber + " to release/v0", body: "Backport #" + parentIssueNumber + " to release/v0" + labels: ['release/v0'] }); const subIssueId = newIssue.data.id; diff --git a/aspell_custom.txt b/aspell_custom.txt index 264427b..aedeb0b 100644 --- a/aspell_custom.txt +++ b/aspell_custom.txt @@ -13,3 +13,4 @@ gorelease repo pr assignees +backport From 5e205dec0c11fe197d6b23c260f34117587f317e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:06:22 -0500 Subject: [PATCH 16/31] fix: bump dependency from 8 to 27 in tools (#29) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.8.0 to 0.27.0. - [Commits](https://github.com/golang/oauth2/compare/v0.8.0...v0.27.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-version: 0.27.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/go.mod | 5 +---- tools/go.sum | 7 ++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index 88a5d45..97086d7 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -30,7 +30,6 @@ require ( github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-github/v45 v45.2.0 // indirect github.com/google/go-github/v53 v53.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -82,13 +81,11 @@ require ( golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.26.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/go.sum b/tools/go.sum index a06f95e..a49c1a7 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -147,7 +147,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -504,8 +503,9 @@ golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -609,7 +609,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -637,8 +636,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba 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.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 6e7bc56d5366baab8a2376cc687730aa5ee88ae4 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 22:14:21 -0500 Subject: [PATCH 17/31] fix: remove try (#31) Signed-off-by: matttrach --- .github/pull_request_template.md | 3 +- .github/workflows/backport-prs.yml | 66 ++++++++++++++---------------- .github/workflows/backport.yml | 1 + .github/workflows/main-issue.yml | 7 ++-- 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 23c2361..7d71e42 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,12 +1,13 @@ ## Related Issue -If this PR will target main, please complete the below sentence. +If this PR will target main, please correct the below sentence. Addresses #1234 (main issue) ## Releases If this PR should be released, please add labels for each release branch it targets. +Use the 'release/v0' tags, not the 'version/v0' tags. ## Description diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index a0b2616..5bcb999 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -41,7 +41,7 @@ jobs: return; } core.info(`Found associated PR: #${pr.number}`); - + core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`); const searchResults = await github.rest.search.issuesAndPullRequests({ q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}` @@ -53,7 +53,7 @@ jobs: } const mainIssue = searchResults.data.items[0]; core.info(`Found main issue: #${mainIssue.number}`); - + core.info(`Fetching sub-issues for main issue #${mainIssue.number}`); const { data: subIssues } = await github.rest.issues.listSubIssues({ owner, @@ -66,41 +66,35 @@ jobs: return; } core.info(`Found ${subIssues.length} sub-issues.`); - - for (const subIssue of subIssues) { - try { - const subIssueNumber = subIssue.number; - - // Find the release label directly on the sub-issue object - const releaseLabel = subIssue.labels.find(label => label.name.startsWith('release/v')); - if (!releaseLabel) { - core.warning(`Sub-issue #${subIssueNumber} has no 'release/v...' label. Skipping.`); - continue; - } - const targetBranch = releaseLabel.name - core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`); - - const newBranchName = `backport-${pr.number}-${targetBranch.replace(/\//g, '-')}`; - execSync(`git config user.name "github-actions[bot]"`); - execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`); - execSync(`git fetch origin ${targetBranch}`); - execSync(`git checkout -b ${newBranchName} origin/${targetBranch}`); - core.info(`Cherry-picking commit ${mergeCommitSha}...`); - execSync(`git cherry-pick -x ${mergeCommitSha}`); + for (const subIssue of subIssues) { + const subIssueNumber = subIssue.number; + + // Find the release label directly on the sub-issue object + const releaseLabel = subIssue.labels.find(label => label.name.startsWith('release/v')); + if (!releaseLabel) { + core.warning(`Sub-issue #${subIssueNumber} has no 'release/v...' label. Skipping.`); + continue; + } + const targetBranch = releaseLabel.name + core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`); - core.info(`Pushing new branch ${newBranchName}...`); - execSync(`git push origin ${newBranchName}`); - - core.info(`Creating pull request for branch ${newBranchName} targeting ${targetBranch}...`); + const newBranchName = `backport-${pr.number}-${targetBranch.replace(/\//g, '-')}`; + execSync(`git config user.name "github-actions[bot]"`); + execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`); + execSync(`git fetch origin ${targetBranch}`); + execSync(`git checkout -b ${newBranchName} origin/${targetBranch}`); + execSync(`git cherry-pick -x ${mergeCommitSha}`); + execSync(`git push origin ${newBranchName}`); - const { data: newPr } = await github.rest.pulls.create({ - owner, - repo, - title: pr.title, - head: newBranchName, - base: targetBranch, - body: "This pull request cherry-picks the changes from #" + pr.number + " into " + targetBranch, - }); - } + core.info(`Creating pull request for branch ${newBranchName} targeting ${targetBranch}...`); + const { data: newPr } = await github.rest.pulls.create({ + owner, + repo, + title: pr.title, + head: newBranchName, + base: targetBranch, + body: "This pull request cherry-picks the changes from #" + pr.number + " into " + targetBranch, + assignees: ['terraform-maintainers'] + }); } diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 270cc0a..0c51f8f 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -26,6 +26,7 @@ jobs: title: "Backport #" + parentIssueNumber + " to release/v0", body: "Backport #" + parentIssueNumber + " to release/v0" labels: ['release/v0'] + assignees: ['terraform-maintainers'] }); const subIssueId = newIssue.data.id; diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index c760c03..0e60130 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -18,7 +18,8 @@ jobs: const repo = context.repo.repo; const owner = context.repo.owner; const pr = context.payload.pull_request; - const reviewers = pr.requested_reviewers.map(reviewer => reviewer.login) + const releaseLabel = pr.head.labels.find(label => label.name.startsWith('release/v')); + const versionLabel = releaseLabel.name.replace('release/', 'version/'); // Create the main issue const newIssue = await github.rest.issues.create({ @@ -29,6 +30,6 @@ jobs: "Please add labels indicating the release versions eg. 'version/v0' \n\n" + "Please add comments for user issues which this issue addresses. \n\n" + "Description copied from PR: \n" + pr.body, - labels: ['internal/main'], - assignees: reviewers + labels: ['internal/main', versionLabel], + assignees: ['terraform-maintainers'] }); From 3cb32e0d3da37d212979db230de39911a47fa3d4 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 22:28:37 -0500 Subject: [PATCH 18/31] fix: use rest request to get sub issues (#33) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index 5bcb999..13d1721 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -34,7 +34,6 @@ jobs: repo, commit_sha: mergeCommitSha, }); - const pr = associatedPrs.find(p => p.base.ref === 'main' && p.merged_at); if (!pr) { core.info(`No merged PR found for commit ${mergeCommitSha}. This may have been a direct push. Exiting.`); @@ -46,7 +45,6 @@ jobs: const searchResults = await github.rest.search.issuesAndPullRequests({ q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}` }); - if (searchResults.data.items.length === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); return; @@ -55,12 +53,14 @@ jobs: core.info(`Found main issue: #${mainIssue.number}`); core.info(`Fetching sub-issues for main issue #${mainIssue.number}`); - const { data: subIssues } = await github.rest.issues.listSubIssues({ - owner, - repo, + const { data: subIssues } = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/sub-issues', { + owner: owner, + repo: repo, issue_number: mainIssue.number, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } }); - if (subIssues.length === 0) { core.info(`No sub-issues found for issue #${mainIssue.number}. Exiting.`); return; @@ -77,8 +77,8 @@ jobs: continue; } const targetBranch = releaseLabel.name - core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`); + core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`); const newBranchName = `backport-${pr.number}-${targetBranch.replace(/\//g, '-')}`; execSync(`git config user.name "github-actions[bot]"`); execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`); From c4dad54b852a60ec115f2309eaa4bb1c78751912 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 22:43:31 -0500 Subject: [PATCH 19/31] fix: use new search API and handle empty label (#34) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 2 +- .github/workflows/main-issue.yml | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index 13d1721..a8b5a0e 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -42,7 +42,7 @@ jobs: core.info(`Found associated PR: #${pr.number}`); core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`); - const searchResults = await github.rest.search.issuesAndPullRequests({ + const searchResults = await github.rest.search.issues({ q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}` }); if (searchResults.data.items.length === 0) { diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index 0e60130..98c496f 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -19,7 +19,11 @@ jobs: const owner = context.repo.owner; const pr = context.payload.pull_request; const releaseLabel = pr.head.labels.find(label => label.name.startsWith('release/v')); - const versionLabel = releaseLabel.name.replace('release/', 'version/'); + if releaseLabel { + const versionLabel = releaseLabel.name.replace('release/', 'version/'); + } else { + const versionLabel = '' + } // Create the main issue const newIssue = await github.rest.issues.create({ From 147a1e4509e08aefc75054cd90d88caf53e10cc9 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 23:38:36 -0500 Subject: [PATCH 20/31] fix: remove backport info from pr template (#35) Signed-off-by: matttrach --- .github/pull_request_template.md | 9 --------- flake.lock | 6 +++--- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7d71e42..464b0b5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,5 @@ ## Related Issue -If this PR will target main, please correct the below sentence. - Addresses #1234 (main issue) ## Releases @@ -22,10 +20,3 @@ Please describe how you verified this change or why testing isn't relevant. Does this change alter an interface that users of the provider will need to adjust to? Will there be any existing configurations broken by this change? -## Backport - -Is this a cherry-picked backport from the default branch? -If so, please delete all other sections and complete the sentence below: - -Cherry-pick #1236 (main PR) to release/v1 (release branch) -Addresses #1235 (backport issue) for #1234 (main issue) diff --git a/flake.lock b/flake.lock index b999921..af58f08 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755113249, - "narHash": "sha256-/bIVS2iP5mixEQWsaiiJ7EGLtk5Id9OehWbmTbzN6kE=", + "lastModified": 1755577059, + "narHash": "sha256-5hYhxIpco8xR+IpP3uU56+4+Bw7mf7EMyxS/HqUYHQY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e9e0d35e5f735bf3d1e96815272f46fe7083232c", + "rev": "97eb7ee0da337d385ab015a23e15022c865be75c", "type": "github" }, "original": { From a9b314efd486e03d35dcf32a30f65d07ea1289dd Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Tue, 19 Aug 2025 23:44:26 -0500 Subject: [PATCH 21/31] fix: create a new array to save labels (#36) Signed-off-by: matttrach --- .github/workflows/main-issue.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index 98c496f..8f9c976 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -18,13 +18,12 @@ jobs: const repo = context.repo.repo; const owner = context.repo.owner; const pr = context.payload.pull_request; + const newLabels = ['internal/main'] const releaseLabel = pr.head.labels.find(label => label.name.startsWith('release/v')); - if releaseLabel { + if (releaseLabel) { const versionLabel = releaseLabel.name.replace('release/', 'version/'); - } else { - const versionLabel = '' + newLabels.push(versionLabel) } - // Create the main issue const newIssue = await github.rest.issues.create({ owner: owner, @@ -34,6 +33,6 @@ jobs: "Please add labels indicating the release versions eg. 'version/v0' \n\n" + "Please add comments for user issues which this issue addresses. \n\n" + "Description copied from PR: \n" + pr.body, - labels: ['internal/main', versionLabel], + labels: newLabels, assignees: ['terraform-maintainers'] }); From d63321b3e3b4b533403a3fe2da39e897359fce99 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 09:14:32 -0500 Subject: [PATCH 22/31] fix: use API directly to query issues (#37) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index a8b5a0e..2bb1835 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -42,8 +42,9 @@ jobs: core.info(`Found associated PR: #${pr.number}`); core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`); - const searchResults = await github.rest.search.issues({ - q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}` + const { data: searchResults } = await github.request('GET /search/issues', { + q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}`, + advanced_search: true }); if (searchResults.data.items.length === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); From f7af5f97cdff11919a361467df7d00e273a866c5 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 09:25:45 -0500 Subject: [PATCH 23/31] fix: add console log to check context (#38) Signed-off-by: matttrach --- .github/workflows/main-issue.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index 8f9c976..be4b9d8 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -15,11 +15,13 @@ jobs: - uses: actions/github-script@v7 with: script: | + console.log("The full event context:", JSON.stringify(context, null, 2)); + const repo = context.repo.repo; const owner = context.repo.owner; const pr = context.payload.pull_request; const newLabels = ['internal/main'] - const releaseLabel = pr.head.labels.find(label => label.name.startsWith('release/v')); + const releaseLabel = pr.labels.find(label => label.name.startsWith('release/v')); if (releaseLabel) { const versionLabel = releaseLabel.name.replace('release/', 'version/'); newLabels.push(versionLabel) From e9ac9a95c3e5a1abe6507ce9f517906a0310cab1 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 11:58:05 -0500 Subject: [PATCH 24/31] fix: remove console log and change count property (#39) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 13 ++++++++----- .github/workflows/main-issue.yml | 2 -- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index 2bb1835..25376cc 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -15,11 +15,12 @@ jobs: actions: read steps: + - name: 'Wait for merge to settle' + run: sleep 10 - name: 'Checkout Repository' uses: actions/checkout@v4 with: fetch-depth: 0 - - name: 'Find Issues and Create Cherry-Pick PRs' uses: actions/github-script@v7 with: @@ -41,18 +42,22 @@ jobs: } core.info(`Found associated PR: #${pr.number}`); + // https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`); const { data: searchResults } = await github.request('GET /search/issues', { q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}`, - advanced_search: true + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } }); - if (searchResults.data.items.length === 0) { + if (searchResults.data.total_count === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); return; } const mainIssue = searchResults.data.items[0]; core.info(`Found main issue: #${mainIssue.number}`); + // https://docs.github.com/en/rest/issues/sub-issues?apiVersion=2022-11-28#add-sub-issue core.info(`Fetching sub-issues for main issue #${mainIssue.number}`); const { data: subIssues } = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/sub-issues', { owner: owner, @@ -70,7 +75,6 @@ jobs: for (const subIssue of subIssues) { const subIssueNumber = subIssue.number; - // Find the release label directly on the sub-issue object const releaseLabel = subIssue.labels.find(label => label.name.startsWith('release/v')); if (!releaseLabel) { @@ -78,7 +82,6 @@ jobs: continue; } const targetBranch = releaseLabel.name - core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`); const newBranchName = `backport-${pr.number}-${targetBranch.replace(/\//g, '-')}`; execSync(`git config user.name "github-actions[bot]"`); diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index be4b9d8..baabde5 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -15,8 +15,6 @@ jobs: - uses: actions/github-script@v7 with: script: | - console.log("The full event context:", JSON.stringify(context, null, 2)); - const repo = context.repo.repo; const owner = context.repo.owner; const pr = context.payload.pull_request; From da1738bbb0598cf3102709f1c24465cb1e9b5bc1 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 12:22:26 -0500 Subject: [PATCH 25/31] fix: add team members individually to issue (#40) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 4 +++- .github/workflows/backport.yml | 13 ++++++++++--- .github/workflows/main-issue.yml | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index 25376cc..4f29843 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -33,7 +33,7 @@ jobs: const { data: associatedPrs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner, repo, - commit_sha: mergeCommitSha, + commit_sha: mergeCommitSha }); const pr = associatedPrs.find(p => p.base.ref === 'main' && p.merged_at); if (!pr) { @@ -46,10 +46,12 @@ jobs: core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`); const { data: searchResults } = await github.request('GET /search/issues', { q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}`, + advanced_search: true, headers: { 'X-GitHub-Api-Version': '2022-11-28' } }); + core.info(`Search results: ${searchResults}`) if (searchResults.data.total_count === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); return; diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 0c51f8f..9bf76a9 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -19,14 +19,21 @@ jobs: const repo = context.repo.repo; const owner = context.repo.owner; + // Get terraform-maintainers team + const { data: teamMembers } = await github.rest.teams.listMembersInOrg({ + org: "rancher", + team_slug: "terraform-maintainers" + }); + const newAssignees = teamMembers.map(member => member.login); + // Create the sub-issue const newIssue = await github.rest.issues.create({ owner: owner, repo: repo, title: "Backport #" + parentIssueNumber + " to release/v0", - body: "Backport #" + parentIssueNumber + " to release/v0" - labels: ['release/v0'] - assignees: ['terraform-maintainers'] + body: "Backport #" + parentIssueNumber + " to release/v0", + labels: ['release/v0'], + assignees: newAssignees // assign terraform-maintainers to sub-issue }); const subIssueId = newIssue.data.id; diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index baabde5..12c0d0a 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -18,13 +18,22 @@ jobs: const repo = context.repo.repo; const owner = context.repo.owner; const pr = context.payload.pull_request; - const newLabels = ['internal/main'] + const newLabels = ['internal/main']; const releaseLabel = pr.labels.find(label => label.name.startsWith('release/v')); if (releaseLabel) { const versionLabel = releaseLabel.name.replace('release/', 'version/'); - newLabels.push(versionLabel) + newLabels.push(versionLabel); } + // Get terraform-maintainers team + const { data: teamMembers } = await github.rest.teams.listMembersInOrg({ + org: "rancher", + team_slug: "terraform-maintainers" + }); + const newAssignees = teamMembers.map(member => member.login); + // Create the main issue + // https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#create-an-issue + // Note: issues can't have teams assigned to them const newIssue = await github.rest.issues.create({ owner: owner, repo: repo, @@ -34,5 +43,5 @@ jobs: "Please add comments for user issues which this issue addresses. \n\n" + "Description copied from PR: \n" + pr.body, labels: newLabels, - assignees: ['terraform-maintainers'] + assignees: newAssignees // assign terraform-maintainers to issue }); From d59cbcd61aba8d43de8201e1dea6d0aa8c530e2a Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 13:08:56 -0500 Subject: [PATCH 26/31] fix: remove console line (#41) * fix: remove console line * fix: assign to me --------- Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 1 - .github/workflows/backport.yml | 10 ++-------- .github/workflows/main-issue.yml | 10 ++-------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index 4f29843..fcaf0b7 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -51,7 +51,6 @@ jobs: 'X-GitHub-Api-Version': '2022-11-28' } }); - core.info(`Search results: ${searchResults}`) if (searchResults.data.total_count === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); return; diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 9bf76a9..79469a8 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -19,13 +19,7 @@ jobs: const repo = context.repo.repo; const owner = context.repo.owner; - // Get terraform-maintainers team - const { data: teamMembers } = await github.rest.teams.listMembersInOrg({ - org: "rancher", - team_slug: "terraform-maintainers" - }); - const newAssignees = teamMembers.map(member => member.login); - + // Note: can't get terraform-maintainers team, the default token can't access org level objects // Create the sub-issue const newIssue = await github.rest.issues.create({ owner: owner, @@ -33,7 +27,7 @@ jobs: title: "Backport #" + parentIssueNumber + " to release/v0", body: "Backport #" + parentIssueNumber + " to release/v0", labels: ['release/v0'], - assignees: newAssignees // assign terraform-maintainers to sub-issue + assignees: ['matttrach'] }); const subIssueId = newIssue.data.id; diff --git a/.github/workflows/main-issue.yml b/.github/workflows/main-issue.yml index 12c0d0a..701b50c 100644 --- a/.github/workflows/main-issue.yml +++ b/.github/workflows/main-issue.yml @@ -24,13 +24,7 @@ jobs: const versionLabel = releaseLabel.name.replace('release/', 'version/'); newLabels.push(versionLabel); } - // Get terraform-maintainers team - const { data: teamMembers } = await github.rest.teams.listMembersInOrg({ - org: "rancher", - team_slug: "terraform-maintainers" - }); - const newAssignees = teamMembers.map(member => member.login); - + // Note: can't get terraform-maintainers team, the default token can't access org level objects // Create the main issue // https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#create-an-issue // Note: issues can't have teams assigned to them @@ -43,5 +37,5 @@ jobs: "Please add comments for user issues which this issue addresses. \n\n" + "Description copied from PR: \n" + pr.body, labels: newLabels, - assignees: newAssignees // assign terraform-maintainers to issue + assignees: ['matttrach'] }); From 91ed4c1e6d92d90b46ead078b1a775ad311ee602 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 13:27:09 -0500 Subject: [PATCH 27/31] fix: add console line converting object to string (#42) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index fcaf0b7..a9a3519 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -51,6 +51,7 @@ jobs: 'X-GitHub-Api-Version': '2022-11-28' } }); + console.log("The search results:", JSON.stringify(searchResults, null, 2)); if (searchResults.data.total_count === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); return; From f12f1c05f50d492692ba8d7d2f8c7e4b7149e864 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 13:54:50 -0500 Subject: [PATCH 28/31] fix: remove unnecessary data (#45) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index a9a3519..371448a 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -52,11 +52,11 @@ jobs: } }); console.log("The search results:", JSON.stringify(searchResults, null, 2)); - if (searchResults.data.total_count === 0) { + if (searchResults.total_count === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); return; } - const mainIssue = searchResults.data.items[0]; + const mainIssue = searchResults.items[0]; core.info(`Found main issue: #${mainIssue.number}`); // https://docs.github.com/en/rest/issues/sub-issues?apiVersion=2022-11-28#add-sub-issue From 5c220916e472616dbe55604c06b158428cdb0ede Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 14:27:41 -0500 Subject: [PATCH 29/31] fix: correct sub issue address (#47) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 3 +-- aspell_custom.txt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index 371448a..1f76eea 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -51,7 +51,6 @@ jobs: 'X-GitHub-Api-Version': '2022-11-28' } }); - console.log("The search results:", JSON.stringify(searchResults, null, 2)); if (searchResults.total_count === 0) { core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); return; @@ -61,7 +60,7 @@ jobs: // https://docs.github.com/en/rest/issues/sub-issues?apiVersion=2022-11-28#add-sub-issue core.info(`Fetching sub-issues for main issue #${mainIssue.number}`); - const { data: subIssues } = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/sub-issues', { + const { data: subIssues } = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { owner: owner, repo: repo, issue_number: mainIssue.number, diff --git a/aspell_custom.txt b/aspell_custom.txt index aedeb0b..e14e570 100644 --- a/aspell_custom.txt +++ b/aspell_custom.txt @@ -14,3 +14,4 @@ repo pr assignees backport +url From b79b58c5d4e4d8ca1c1fcba467a290cca0172df9 Mon Sep 17 00:00:00 2001 From: Matt Trachier Date: Wed, 20 Aug 2025 16:04:15 -0500 Subject: [PATCH 30/31] fix: resolve merge conflicts in backport (#50) Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index 1f76eea..f2527df 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -89,7 +89,7 @@ jobs: execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`); execSync(`git fetch origin ${targetBranch}`); execSync(`git checkout -b ${newBranchName} origin/${targetBranch}`); - execSync(`git cherry-pick -x ${mergeCommitSha}`); + execSync(`git cherry-pick -x ${mergeCommitSha} -X theirs`); execSync(`git push origin ${newBranchName}`); core.info(`Creating pull request for branch ${newBranchName} targeting ${targetBranch}...`); @@ -99,7 +99,9 @@ jobs: title: pr.title, head: newBranchName, base: targetBranch, - body: "This pull request cherry-picks the changes from #" + pr.number + " into " + targetBranch, + body: "This pull request cherry-picks the changes from #" + pr.number + " into " + targetBranch + "\n" + + "WARNING!: to avoid having to resolve merge conflicts this PR is generated with `git cherry-pick -X theirs`.\n" + + "Please make sure to carefully inspect this PR so that you don't revert anything!", assignees: ['terraform-maintainers'] }); } From 52fbc9bc20a084bfbf703e15c44c6a00a81739f0 Mon Sep 17 00:00:00 2001 From: matttrach Date: Wed, 20 Aug 2025 16:19:41 -0500 Subject: [PATCH 31/31] fix: rephrase cherry pick workflow Signed-off-by: matttrach --- .github/workflows/backport-prs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml index f2527df..0c211fc 100644 --- a/.github/workflows/backport-prs.yml +++ b/.github/workflows/backport-prs.yml @@ -2,8 +2,7 @@ name: 'Auto Cherry-Pick to Release Branches' on: push: - branches: - - 'main' + branches: ['main'] jobs: create-cherry-pick-prs: