Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf8681305 | ||
|
|
047945cabf |
@@ -20,6 +20,19 @@ regress.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-01: Built images are tagged `upaas-<app>:<short hash>`, git's short
|
||||
form of the commit built, instead of the deployment number. A redeploy of a
|
||||
commit gives its tag to the new image; the old one is kept while the app runs
|
||||
it or Rollback would start it, then removed by its ID, found among the images
|
||||
the app's deployments recorded. The removal of old images now keeps every
|
||||
image any app runs or would roll back to. A deployment's commit is now saved
|
||||
when it is updated, so manual deploys keep the commit read from the clone
|
||||
(#239).
|
||||
|
||||
- 2026-10-01: Two database writes at the same moment no longer fail with
|
||||
"database is locked": each transaction now takes the write lock when it begins
|
||||
and waits up to 5 seconds for another writer to finish (#253).
|
||||
|
||||
- 2026-10-01: The hint under the app page's environment variable editor now says
|
||||
changes take effect at the next deploy or rollback, in the page's warning
|
||||
style, instead of asking for a container restart, which keeps the old values
|
||||
@@ -42,15 +55,6 @@ regress.
|
||||
create it on the host first. The README has a Volume mounts section saying so
|
||||
(#235).
|
||||
|
||||
- 2026-09-29: Built images are tagged `upaas-<app>:<short hash>`, git's short
|
||||
form of the commit built, instead of the deployment number. A redeploy of a
|
||||
commit gives its tag to the new image; the old one is kept while the app runs
|
||||
it or Rollback would start it, then removed by its ID, found among the images
|
||||
the app's deployments recorded. The removal of old images now keeps every
|
||||
image any app runs or would roll back to. A deployment's commit is now saved
|
||||
when it is updated, so manual deploys keep the commit read from the clone
|
||||
(#239).
|
||||
|
||||
- 2026-09-29: The app page is 50% wider on large screens (84rem instead of
|
||||
56rem), its build log and container log boxes are twice as tall, the build log
|
||||
sits between the webhook URL and the environment variables, and the container
|
||||
|
||||
@@ -137,8 +137,11 @@ func (d *Database) connect(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
// Open database with WAL mode and foreign keys
|
||||
dsn := dbPath + "?_journal_mode=WAL&_foreign_keys=on"
|
||||
// Open database with WAL mode and foreign keys. Transactions take the
|
||||
// write lock when they begin and wait up to 5s for another writer,
|
||||
// instead of failing with "database is locked" when both write.
|
||||
dsn := dbPath + "?_journal_mode=WAL&_foreign_keys=on" +
|
||||
"&_txlock=immediate&_busy_timeout=5000"
|
||||
|
||||
database, err := sql.Open("sqlite3", dsn)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
@@ -610,3 +611,72 @@ func TestPerformCloneFailsWithoutShortSHA(t *testing.T) {
|
||||
t.Errorf("got error %v, want %v", err, ErrGitCloneFailed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPerformCloneAsksGitForShortSHA runs a clone of a branch's last commit
|
||||
// and a clone of a given commit against a fake Docker API, and checks that
|
||||
// the command each clone container is created with prints git's own short
|
||||
// form of the commit checked out.
|
||||
func TestPerformCloneAsksGitForShortSHA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
commitSHA string
|
||||
}{
|
||||
{name: "branch", commitSHA: ""},
|
||||
{name: "commit", commitSHA: cloneCommit},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
created := make(chan container.Config, 1)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/containers/create"):
|
||||
var cfg container.Config
|
||||
|
||||
_ = json.NewDecoder(r.Body).Decode(&cfg)
|
||||
created <- cfg
|
||||
|
||||
_, _ = w.Write([]byte(`{"Id":"gitcontainer"}`))
|
||||
case strings.HasSuffix(r.URL.Path, "/wait"):
|
||||
_, _ = w.Write([]byte(`{"StatusCode":0}`))
|
||||
case strings.HasSuffix(r.URL.Path, "/logs"):
|
||||
writeCloneOutput(w)
|
||||
default:
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}
|
||||
},
|
||||
))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
dockerAPI, err := client.NewClientWithOpts(
|
||||
client.WithHost("tcp://" + srv.Listener.Addr().String()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := &Client{docker: dockerAPI, log: slog.Default()}
|
||||
|
||||
cfg := testCloneConfig(t)
|
||||
cfg.commitSHA = tt.commitSHA
|
||||
|
||||
_, err = c.performClone(t.Context(), cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := strings.Join((<-created).Cmd, " ")
|
||||
if !strings.Contains(cmd, "echo SHORT_SHA:$(git rev-parse --short HEAD)") {
|
||||
t.Errorf("clone command %q does not print git's short hash", cmd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +246,54 @@ func TestRecordDeployedImageRemovesOldImages(t *testing.T) {
|
||||
assert.False(t, api.forced, "old images must be removed without force")
|
||||
}
|
||||
|
||||
// TestRecordDeployedImageRemovesOnlyTheAppsUntaggedImages runs the step after
|
||||
// a deploy against a fake Docker API that holds three untagged images: one an
|
||||
// earlier deployment of the app recorded, one a deployment of another app
|
||||
// recorded, and one no deployment recorded, such as an image upaas never
|
||||
// built. Only the app's own is removed.
|
||||
func TestRecordDeployedImageRemovesOnlyTheAppsUntaggedImages(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
api := &fakeImageAPI{images: map[string][]string{
|
||||
"sha256:new": {"upaas-myapp:1a2b3c4"},
|
||||
"sha256:myapp": {},
|
||||
"sha256:otherapp": {},
|
||||
"sha256:unknown": {},
|
||||
}}
|
||||
svc, db := newImageTestService(t, api)
|
||||
ctx := context.Background()
|
||||
|
||||
app := saveApp(t, db, "myapp", "", "")
|
||||
otherApp := saveApp(t, db, "otherapp", "", "")
|
||||
|
||||
saveDeployment := func(appID, imageID string) *models.Deployment {
|
||||
t.Helper()
|
||||
|
||||
deployment := models.NewDeployment(db)
|
||||
deployment.AppID = appID
|
||||
deployment.ImageID = sql.NullString{String: imageID, Valid: true}
|
||||
require.NoError(t, deployment.Save(ctx))
|
||||
|
||||
return deployment
|
||||
}
|
||||
|
||||
saveDeployment(app.ID, "sha256:myapp")
|
||||
saveDeployment(otherApp.ID, "sha256:otherapp")
|
||||
deployment := saveDeployment(app.ID, "sha256:new")
|
||||
|
||||
err := svc.RecordDeployedImage(ctx, app, deployment, "sha256:new")
|
||||
require.NoError(t, err)
|
||||
|
||||
images, removed := api.state()
|
||||
|
||||
assert.Equal(t, []string{"sha256:myapp"}, removed)
|
||||
assert.Equal(t, map[string][]string{
|
||||
"sha256:new": {"upaas-myapp:1a2b3c4"},
|
||||
"sha256:otherapp": {},
|
||||
"sha256:unknown": {},
|
||||
}, images)
|
||||
}
|
||||
|
||||
// TestRedeployRemovesImagesLeftWithoutTag deploys commits against a fake
|
||||
// Docker API, some of them again. A build takes the commit's tag from the
|
||||
// image an earlier build of it made. That image is kept, without a tag,
|
||||
|
||||
Reference in New Issue
Block a user