There are several options.
The one where you won't potentially have to change your table structures and add extra data or use inefficient methods like updating through a subquery or a view, is a cursor.
Unfortunately you didn't mention what dbms you are using and the syntax is very specific. on MSSQL it would be something like
Declare @ProjectID int
Declare CURSOR csrIncompete AS
Select ProjectID
From Projects
Where Status = 'Incomplete'
Open csrIncomplete
Fetch Next csrIncomplete Into @ProjectID
While @@Fetch_Status = 0
Begin
If Not Exists(Select * From Actions Where ProjectID = @ProjectID and Status <> 'Complete')
Update Projects Set Status = 'Complete' Where ProjectID = @ProjectID
Fetch Next csrIncomplete Into @ProjectID
End
Close crsIncomplete
Deallocate csrIncomplete
When to run the above is of course another question entirely.
The absolute simplest solution is not to store Project Status in the Projects table at all and to derive it from the contents of actions.
For instance if the ProjectStatus is that of the least complete action and action status's are integers. Selecting the min of actionstatus for each project ID in a join and then joining that to a status description table would give you the desrired answer, you'd just have to have an extra code (0 ?) for those projects with no actions and outer join Projects and Actions using the IfNull function to pick up 0 = 'No actions yet Assigned' or some such.
Some DBMS's will let you do this but it's horribly inefficient
Update Projects Set ProjectStatus = 'Complete'
Where ProjectStatus <> 'Complete ' and Not Exists(Select * From Actions Where Actions.ProjectID = Projects.ProjectID and Action.Status <> 'Complete')