Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,62 @@ Before submitting changes, agents should verify the following:

#### Verification steps

- [ ] All tests pass locally (run `go test ./...`).
Before committing, **always** run the following checks:

1. **Run all tests**:
```bash
go test ./...
```

2. **Run linting and formatting**:
```bash
pnpm check
```

If both pass, proceed with the commit. If either fails, fix the issues before committing.

#### Handling network issues during testing

If you encounter network errors when running `go test ./...` or `pnpm check` (e.g., "dial tcp: lookup
storage.googleapis.com"), follow these steps:

1. **Check the local Go version**:
```bash
GOTOOLCHAIN=local go version
```

2. **Temporarily downgrade `go.mod`** to match the local Go version:
```bash
# If local version is go1.24.7, change go.mod from:
# go 1.25.3
# to:
# go 1.24.7
```

3. **Run tests with the local toolchain**:
```bash
GOTOOLCHAIN=local go test ./... -timeout 5m
```

4. **Verify the code compiles and tests pass** (or skip appropriately).

5. **Restore the original Go version in `go.mod`** before committing:
```bash
# Change back to:
# go 1.25.3
```

6. **CRITICAL: Never commit the downgraded `go.mod` version**. Always restore it to the original
version before staging files.

7. **Alternative formatting check** (if `pnpm check` fails due to network):
```bash
GOTOOLCHAIN=local go fmt ./...
GOTOOLCHAIN=local go vet ./...
```

#### Additional checks

- [ ] Code is properly formatted with `go fmt`.
- [ ] Generated files (`.gen.go`, manifest tracked files) were not manually edited.
- [ ] New exported types, functions, and constants have Go doc comments.
Expand Down
55 changes: 55 additions & 0 deletions internal/infra/tf_appdef_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,61 @@ func TestTerraform_Resources(t *testing.T) {
})
}

func TestTerraform_DefaultB2Bucket(t *testing.T) {
appDef := &appdef.Definition{
Project: appdef.Project{
Name: "project",
Repo: appdef.GitHubRepo{
Owner: "ainsley-dev",
Name: "project",
},
},
Resources: []appdef.Resource{},
Apps: []appdef.App{},
}

tf, teardown := setup(t, appDef)
defer teardown()

err := tf.Init(t.Context())
require.NoError(t, err)

got, err := tf.Plan(t.Context(), env.Production)
require.NoError(t, err)
require.NotNil(t, got)

t.Log("Default B2 Bucket Configuration")
{
var b2Bucket map[string]any
for _, rc := range got.Plan.ResourceChanges {
if rc.Type == "b2_bucket" && rc.Name == "this" {
b2Bucket = rc.Change.After.(map[string]any)
break
}
}
require.NotNil(t, b2Bucket, "B2 bucket resource should be planned")

assert.Equal(t, "project", b2Bucket["bucket_name"])
assert.Equal(t, "allPrivate", b2Bucket["bucket_type"])

// Verify lifecycle rules for single version
lifecycleRules := b2Bucket["lifecycle_rules"].([]any)
require.Len(t, lifecycleRules, 1, "Should have exactly one lifecycle rule")

rule := lifecycleRules[0].(map[string]any)
assert.Equal(t, float64(1), rule["days_from_hiding_to_deleting"], "Should delete old versions after 1 day")
assert.Equal(t, float64(0), rule["days_from_uploading_to_hiding"], "Should hide old versions immediately")
assert.Equal(t, "", rule["file_name_prefix"], "Should apply to all files")
}

t.Log("Default B2 Bucket Output")
{
defaultBucket := got.Plan.PlannedValues.Outputs["default_b2_bucket"]
require.NotNil(t, defaultBucket, "Default B2 bucket output should exist")
assert.True(t, defaultBucket.Sensitive, "Default B2 bucket output should be sensitive")
}
}

func TestTerraform_Apps(t *testing.T) {
t.Skip()

Expand Down
24 changes: 0 additions & 24 deletions internal/infra/tf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,18 +196,6 @@ func TestTerraform_Plan(t *testing.T) {
assert.ErrorContains(t, err, "terraform not initialized")
})

t.Run("Nothing To Provision", func(t *testing.T) {
tf, teardown := setup(t, &appdef.Definition{})
defer teardown()

err := tf.Init(t.Context())
require.NoError(t, err)

_, err = tf.Plan(t.Context(), env.Production)
assert.Error(t, err)
assert.ErrorContains(t, err, "no app or resources are defined")
})

t.Run("Vars FS Error", func(t *testing.T) {
tf, teardown := setup(t, appDef)
defer teardown()
Expand Down Expand Up @@ -320,18 +308,6 @@ func TestTerraform_Apply(t *testing.T) {
assert.ErrorContains(t, err, "terraform not initialized")
})

t.Run("Nothing To Provision", func(t *testing.T) {
tf, teardown := setup(t, &appdef.Definition{})
defer teardown()

err := tf.Init(t.Context())
require.NoError(t, err)

_, err = tf.Apply(t.Context(), env.Production)
assert.Error(t, err)
assert.ErrorContains(t, err, "no app or resources are defined")
})

t.Run("Vars FS Error", func(t *testing.T) {
tf, teardown := setup(t, &appdef.Definition{
Resources: []appdef.Resource{
Expand Down
4 changes: 0 additions & 4 deletions internal/infra/tf_vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ func tfVarsFromDefinition(env env.Environment, def *appdef.Definition) (tfVars,
return tfVars{}, errors.New("definition cannot be nil")
}

if len(def.Apps) == 0 && len(def.Resources) == 0 {
return tfVars{}, errors.New("no app or resources are defined")
}

vars := tfVars{
ProjectName: def.Project.Name,
Environment: env.String(),
Expand Down
13 changes: 9 additions & 4 deletions internal/infra/tf_vars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ func TestTFVarsFromDefinition(t *testing.T) {

t.Run("Empty Definition", func(t *testing.T) {
input := &appdef.Definition{
Project: appdef.Project{Name: "test"},
Project: appdef.Project{Name: "project"},
Apps: []appdef.App{},
Resources: []appdef.Resource{},
}

_, err := tfVarsFromDefinition(env.Development, input)
assert.Error(t, err)
assert.ErrorContains(t, err, "no app or resources are defined")
got, err := tfVarsFromDefinition(env.Production, input)
assert.NoError(t, err)

t.Log("Metadata")
{
assert.Equal(t, "project", got.ProjectName)
assert.Equal(t, "production", got.Environment)
}
})

t.Run("Single Resource", func(t *testing.T) {
Expand Down
14 changes: 13 additions & 1 deletion platform/terraform/base/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ locals {
[for tag in var.tags : lower(tag)]
)


# Shortened environment names.
environment_short_map = {
PRODUCTION = "prod"
Expand All @@ -67,6 +66,19 @@ locals {
environment_short = lookup(local.environment_short_map, upper(var.environment), lower(var.environment))
}

#
# Default B2 Bucket (always provisioned for every project)
#
module "default_b2_bucket" {
source = "../providers/b2/bucket"

bucket_name = var.project_name
acl = "allPrivate"
days_from_hiding_to_deleting = 1
days_from_uploading_to_hiding = 0
lifecycle_rule_file_name_prefix = ""
}

#
# Resources (databases, storage, etc.)
#
Expand Down
13 changes: 13 additions & 0 deletions platform/terraform/base/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
#
# Default B2 Bucket
#
output "default_b2_bucket" {
description = "Default B2 bucket details"
value = {
id = module.default_b2_bucket.id
name = module.default_b2_bucket.name
info = module.default_b2_bucket.info
}
sensitive = true
}

#
# Resources (databases, storage, etc.)
#
Expand Down
10 changes: 5 additions & 5 deletions platform/terraform/base/variables.tf
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
variable "project_name" {
type = string
description = "Name of the client that will be prefixed on all resources"
}

# variable "project_title" {
# type = string
# description = "Nice name of the client that will appear in project settings"
# }
validation {
condition = length(var.project_name) > 0
error_message = "The project_name variable is required and cannot be empty."
}
}

variable "environment" {
type = string
Expand Down
7 changes: 5 additions & 2 deletions platform/terraform/modules/resources/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ module "b2_bucket" {
count = var.platform_provider == "b2" && var.platform_type == "s3" ? 1 : 0
source = "../../providers/b2/bucket"

bucket_name = var.name
acl = try(var.platform_config.acl, null)
bucket_name = var.name
acl = try(var.platform_config.acl, null)
days_from_hiding_to_deleting = try(var.platform_config.days_from_hiding_to_deleting, null)
days_from_uploading_to_hiding = try(var.platform_config.days_from_uploading_to_hiding, null)
lifecycle_rule_file_name_prefix = try(var.platform_config.lifecycle_rule_file_name_prefix, null)
}
9 changes: 9 additions & 0 deletions platform/terraform/providers/b2/bucket/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@
resource "b2_bucket" "this" {
bucket_name = var.bucket_name
bucket_type = var.acl

dynamic "lifecycle_rules" {
for_each = var.lifecycle_rule_file_name_prefix != null ? [1] : []
content {
days_from_hiding_to_deleting = var.days_from_hiding_to_deleting
days_from_uploading_to_hiding = var.days_from_uploading_to_hiding
file_name_prefix = var.lifecycle_rule_file_name_prefix
}
}
}
18 changes: 18 additions & 0 deletions platform/terraform/providers/b2/bucket/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,21 @@ variable "acl" {
type = string
default = "allPrivate"
}

variable "days_from_hiding_to_deleting" {
description = "How long to keep file versions that are not the current version (in days)"
type = number
default = null
}

variable "days_from_uploading_to_hiding" {
description = "Causes files to be hidden automatically after the given number of days"
type = number
default = null
}

variable "lifecycle_rule_file_name_prefix" {
description = "Specifies which files in the bucket the lifecycle rule applies to"
type = string
default = null
}
Loading